my personal site
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update documentation and enhance API configuration

- Revised ACCESS_SETUP.md to clarify that the Gym Tracker app fetches ads without authentication.
- Updated README.md to reflect the correct repository name and added a new section for the ads landing page.
- Modified worker-configuration.d.ts to include a new ASSETS binding for improved asset management.
- Enhanced wrangler.jsonc with asset configuration and additional route patterns for better API handling.
- Integrated new landing page functionality in index.ts to serve main and ads landing HTML content.

+2741 -508
+501
ads-landing-style-guide.md
··· 1 + # VT Gym Tracker Ads — Style Guide 2 + 3 + Full reference for design tokens and component classes used on the `/ads` landing page. 4 + 5 + --- 6 + 7 + ## Design Tokens 8 + 9 + All tokens are defined as CSS custom properties on `:root`. 10 + 11 + ### Color 12 + 13 + | Token | Value | Usage | 14 + |---|---|---| 15 + | `--maroon` | `#861F41` | Primary brand color, links, accents | 16 + | `--maroon-dark` | `#5c1530` | Hover state for maroon elements | 17 + | `--maroon-deep` | `#3a0d1e` | Hero/nav/footer backgrounds | 18 + | `--orange` | `#E87722` | CTAs, highlights, eyebrows | 19 + | `--orange-dark` | `#c4611a` | Hover state for orange elements | 20 + | `--bg` | `#f8f5f2` | Page background, alternating sections | 21 + | `--surface` | `#ffffff` | Cards, inputs, elevated surfaces | 22 + | `--border` | `#e8e3de` | Default borders | 23 + | `--border-strong` | `#d0c9c3` | Form inputs, emphasized borders | 24 + | `--text` | `#1a1614` | Primary body text | 25 + | `--text-mid` | `#4a4240` | Secondary text, form labels | 26 + | `--muted` | `#7a7270` | Captions, hints, placeholders | 27 + 28 + ### Typography 29 + 30 + | Token | Value | 31 + |---|---| 32 + | `--font-display` | `'Bebas Neue', sans-serif` | 33 + | `--font-body` | `'Plus Jakarta Sans', system-ui, sans-serif` | 34 + 35 + Both fonts load from Google Fonts. Always declare `font-family: var(--font-body)` on `body` and apply `var(--font-display)` only to headings and stat values. 36 + 37 + ### Type Scale 38 + 39 + | Role | Size | Weight | Font | 40 + |---|---|---|---| 41 + | Hero h1 | `clamp(3rem, 8vw, 6rem)` | 400 (Bebas) | Display | 42 + | Section title | `2.75rem` | 400 (Bebas) | Display | 43 + | Format card name | `1.5rem` | 400 (Bebas) | Display | 44 + | Tier button label | `1.1rem` | 400 (Bebas) | Display | 45 + | Hero stat value | `2.5rem` | 400 (Bebas) | Display | 46 + | Body / base | `16px` | 400 | Body | 47 + | Section sub | `1rem` | 400 | Body | 48 + | Feature title | `0.9375rem` | 700 | Body | 49 + | Form label | `0.8125rem` | 600 | Body | 50 + | Caption / hint / muted | `0.8rem–0.875rem` | 400–500 | Body | 51 + | Eyebrow / section label | `0.75rem` | 700 | Body | 52 + 53 + Eyebrow labels are always `text-transform: uppercase` with `letter-spacing: 0.14em`. 54 + 55 + ### Spacing 56 + 57 + Vertical section padding is `72px 0`. Internal component gaps use `8px`, `12px`, `16px`, `20px`, or `24px`. Use `rem` for vertical rhythm between components and `px` for internal element gaps. 58 + 59 + ### Border Radius 60 + 61 + The app and admin use squared-off surfaces. All borders use `border-radius: 0` — no rounding on buttons, inputs, cards, or badges. 62 + 63 + --- 64 + 65 + ## Layout 66 + 67 + ### `.container` 68 + 69 + ```css 70 + max-width: 1080px; 71 + margin: 0 auto; 72 + padding: 0 24px; 73 + ``` 74 + 75 + The single layout wrapper. Use on every section's direct child. 76 + 77 + ### `.section` 78 + 79 + ```css 80 + padding: 72px 0; 81 + ``` 82 + 83 + Base section class. Combine with modifiers: 84 + 85 + | Class | Effect | 86 + |---|---| 87 + | `.section` | Transparent background (shows `--bg`) | 88 + | `.section-alt` | `background: var(--surface)` — white | 89 + | `.section-dark` | `background: var(--maroon-deep)` — dark maroon | 90 + 91 + Sections alternate `.section` → `.section-alt` → `.section` → `.section-alt` → `.section-dark` down the page. 92 + 93 + ### `.section-header` 94 + 95 + ```css 96 + margin-bottom: 44px; 97 + ``` 98 + 99 + Wraps `.section-label` + `.section-title` + `.section-sub` at the top of each section. 100 + 101 + --- 102 + 103 + ## Typography Components 104 + 105 + ### `.section-label` 106 + 107 + Eyebrow text above section titles. 108 + 109 + ```css 110 + font-size: 0.75rem; 111 + font-weight: 700; 112 + letter-spacing: 0.14em; 113 + text-transform: uppercase; 114 + color: var(--orange); 115 + margin-bottom: 10px; 116 + ``` 117 + 118 + ### `.section-title` 119 + 120 + ```css 121 + font-family: var(--font-display); 122 + font-size: 2.75rem; 123 + letter-spacing: 0.03em; 124 + color: var(--text); 125 + line-height: 1; 126 + margin-bottom: 12px; 127 + ``` 128 + 129 + Modifier `.on-dark` overrides color to `#fff` for use on dark backgrounds. 130 + 131 + ### `.section-sub` 132 + 133 + ```css 134 + font-size: 1rem; 135 + color: var(--muted); 136 + max-width: 520px; 137 + line-height: 1.7; 138 + ``` 139 + 140 + --- 141 + 142 + ## Navigation 143 + 144 + ### `.nav` 145 + 146 + ```css 147 + position: sticky; 148 + top: 0; 149 + z-index: 100; 150 + background: var(--maroon-deep); 151 + border-bottom: 1px solid rgba(255,255,255,0.08); 152 + ``` 153 + 154 + ### `.nav-inner` 155 + 156 + ```css 157 + display: flex; 158 + align-items: center; 159 + justify-content: space-between; 160 + padding: 14px 0; 161 + ``` 162 + 163 + ### `.nav-logo` 164 + 165 + Bebas Neue, `1.25rem`, `letter-spacing: 0.08em`, white. No underline on hover — use `opacity: 0.85` instead. 166 + 167 + ### `.nav-links a` 168 + 169 + `0.875rem`, weight 500, `rgba(255,255,255,0.6)`. On hover: `color: #fff`. Hidden below `600px`. 170 + 171 + --- 172 + 173 + ## Hero 174 + 175 + ### `.hero` 176 + 177 + ```css 178 + background: var(--maroon-deep); 179 + padding: 72px 0 0; 180 + overflow: hidden; 181 + position: relative; 182 + ``` 183 + 184 + The `::before` pseudo-element adds a radial glow in the top-right: `radial-gradient(circle, rgba(134,31,65,0.5) 0%, transparent 70%)`. 185 + 186 + ### `.hero-eyebrow` 187 + 188 + Same rules as `.section-label` — orange, uppercase, tracked. 189 + 190 + ### `.hero h1` 191 + 192 + ```css 193 + font-family: var(--font-display); 194 + font-size: clamp(3rem, 8vw, 6rem); 195 + line-height: 0.95; 196 + letter-spacing: 0.02em; 197 + color: #fff; 198 + max-width: 680px; 199 + margin-bottom: 24px; 200 + ``` 201 + 202 + Use `<em>` inside for orange accent text: `color: var(--orange); font-style: normal`. 203 + 204 + ### `.hero-sub` 205 + 206 + ```css 207 + font-size: 1.0625rem; 208 + color: rgba(255,255,255,0.65); 209 + max-width: 480px; 210 + margin-bottom: 36px; 211 + line-height: 1.7; 212 + ``` 213 + 214 + ### `.hero-stats` 215 + 216 + Four-column grid pinned to the bottom of the hero, separated by a top border and thin column dividers. Collapses to two columns below `640px`. 217 + 218 + ```css 219 + display: grid; 220 + grid-template-columns: repeat(4, 1fr); 221 + border-top: 1px solid rgba(255,255,255,0.1); 222 + margin-top: 56px; 223 + ``` 224 + 225 + ### `.hero-stat-val` / `.hero-stat-label` 226 + 227 + Value: Bebas Neue, `2.5rem`, white. Label: `0.8125rem`, weight 500, `rgba(255,255,255,0.5)`. 228 + 229 + --- 230 + 231 + ## Buttons 232 + 233 + ### `.btn-primary` 234 + 235 + Orange filled button. Used for the main hero CTA. 236 + 237 + ```css 238 + background: var(--orange); 239 + color: #fff; 240 + font-weight: 700; 241 + font-size: 0.9375rem; 242 + padding: 13px 26px; 243 + border-radius: 0; 244 + ``` 245 + 246 + Hover: `background: var(--orange-dark)`, `transform: translateY(-1px)`. 247 + 248 + ### `.btn-ghost` 249 + 250 + Outlined, for use on dark backgrounds only. 251 + 252 + ```css 253 + color: rgba(255,255,255,0.7); 254 + border: 1px solid rgba(255,255,255,0.2); 255 + font-weight: 500; 256 + font-size: 0.9375rem; 257 + padding: 13px 22px; 258 + border-radius: 0; 259 + ``` 260 + 261 + Hover: `color: #fff`, `border-color: rgba(255,255,255,0.5)`. 262 + 263 + ### `.btn-contact` 264 + 265 + Maroon filled. Used inside the wizard CTA box. 266 + 267 + ```css 268 + background: var(--maroon); 269 + color: #fff; 270 + font-weight: 700; 271 + font-size: 0.9375rem; 272 + padding: 12px 24px; 273 + border-radius: 0; 274 + ``` 275 + 276 + Hover: `background: var(--maroon-dark)`, `transform: translateY(-1px)`. 277 + 278 + ### `.btn-email` 279 + 280 + Orange filled. Used in the contact section. 281 + 282 + ```css 283 + background: var(--orange); 284 + color: #fff; 285 + font-weight: 700; 286 + font-size: 1rem; 287 + padding: 14px 28px; 288 + border-radius: 0; 289 + ``` 290 + 291 + Hover: `background: var(--orange-dark)`. 292 + 293 + --- 294 + 295 + ## Cards 296 + 297 + ### `.format-card` 298 + 299 + ```css 300 + background: var(--bg); 301 + border: 1px solid var(--border); 302 + border-radius: 0; 303 + overflow: hidden; 304 + ``` 305 + 306 + Used in a three-column `auto-fit, minmax(260px, 1fr)` grid. Each card has a `.format-card-preview` (white background, border-bottom) and `.format-card-info` (padding `18px 20px`). 307 + 308 + **`.format-card-name`** — Bebas Neue, `1.5rem`, `var(--maroon)`. 309 + 310 + **`.format-card-desc`** — `0.875rem`, `var(--muted)`, `line-height: 1.6`. 311 + 312 + ### `.feature-card` 313 + 314 + ```css 315 + background: var(--bg); 316 + border: 1px solid var(--border); 317 + border-radius: 0; 318 + padding: 24px; 319 + ``` 320 + 321 + Used in a two-column grid. Each card contains a `.feature-icon`, `.feature-title`, and `.feature-desc`. 322 + 323 + **`.feature-icon`** — `36×36px`, `border-radius: 0`, `background: rgba(134,31,65,0.1)`, flex-centered. SVG icons are `18×18px` with `stroke: #861F41`. 324 + 325 + **`.feature-title`** — `0.9375rem`, weight 700, `var(--text)`. 326 + 327 + **`.feature-desc`** — `0.875rem`, `var(--muted)`, `line-height: 1.65`. 328 + 329 + --- 330 + 331 + ## Wizard 332 + 333 + ### `.wizard-grid` 334 + 335 + Two-column layout: form on the left, preview on the right. 336 + 337 + ```css 338 + display: grid; 339 + grid-template-columns: 1fr 300px; 340 + gap: 40px; 341 + align-items: start; 342 + ``` 343 + 344 + Collapses to single column below `800px`. 345 + 346 + ### Tier Buttons (`.tier-btn`) 347 + 348 + Three-column grid with equal widths. 349 + 350 + ```css 351 + padding: 10px 0; 352 + text-align: center; 353 + border: 1.5px solid var(--border-strong); 354 + border-radius: 0; 355 + background: var(--surface); 356 + font-family: var(--font-display); 357 + font-size: 1.1rem; 358 + letter-spacing: 0.06em; 359 + color: var(--muted); 360 + ``` 361 + 362 + Each button contains a `<span>` subtitle: body font, `0.75rem`, weight 500. 363 + 364 + **Active state** (`.tier-btn.active`): 365 + ```css 366 + border-color: var(--maroon); 367 + background: var(--maroon); 368 + color: #fff; 369 + /* span color: rgba(255,255,255,0.75) */ 370 + ``` 371 + 372 + ### Form Elements 373 + 374 + **`.form-label`** 375 + ```css 376 + font-size: 0.8125rem; 377 + font-weight: 600; 378 + color: var(--text-mid); 379 + margin-bottom: 7px; 380 + letter-spacing: 0.01em; 381 + ``` 382 + 383 + **`.form-input`** 384 + ```css 385 + width: 100%; 386 + padding: 10px 14px; 387 + border: 1px solid var(--border-strong); 388 + border-radius: 0; 389 + font-size: 0.9375rem; 390 + background: var(--surface); 391 + color: var(--text); 392 + ``` 393 + 394 + Focus ring: `border-color: var(--maroon)`, `box-shadow: 0 0 0 3px rgba(134,31,65,0.1)`. 395 + 396 + Placeholder color: `#b5afaa`. 397 + 398 + **`.form-hint`** — `0.8rem`, `var(--muted)`, `margin-top: 5px`. 399 + 400 + **`.image-group-hidden`** — `display: none`. Applied to the image URL group when Text tier is active. 401 + 402 + ### `.wizard-cta-box` 403 + 404 + ```css 405 + margin-top: 32px; 406 + padding: 22px; 407 + background: rgba(134,31,65,0.05); 408 + border: 1px solid rgba(134,31,65,0.15); 409 + border-radius: 0; 410 + ``` 411 + 412 + Inner `<p>`: `0.9rem`, `var(--text-mid)`, `line-height: 1.65`. 413 + 414 + --- 415 + 416 + ## Ad Preview 417 + 418 + ### `.preview-pane` 419 + 420 + ```css 421 + position: sticky; 422 + top: 84px; 423 + ``` 424 + 425 + Sticky offset accounts for the nav height (`~56px`) plus buffer. 426 + 427 + ### `.preview-device` 428 + 429 + Simulates an app chrome frame. 430 + 431 + ```css 432 + background: var(--bg); 433 + border: 1px solid var(--border); 434 + border-radius: 0; 435 + padding: 16px; 436 + overflow: hidden; 437 + ``` 438 + 439 + **`.preview-device-bar`** — flex row, `margin-bottom: 12px`. Contains `.preview-device-title` (weight 700, `0.8125rem`, `var(--text-mid)`) and three `.preview-device-dot` circles (`6×6px`, `var(--border-strong)`). 440 + 441 + ### `.preview-card` 442 + 443 + ```css 444 + background: var(--surface); 445 + border: 1px solid var(--border); 446 + border-radius: 0; 447 + overflow: hidden; 448 + ``` 449 + 450 + **Image wrap** — `.preview-card-img-wrap`: `overflow: hidden`, gradient placeholder background. Height is `80px` for Banner, `120px` for Feature. 451 + 452 + **Body** — `.preview-card-body`: `padding: 14px 16px`. 453 + 454 + **Sponsor row** — `.preview-sponsor-row`: flex, `gap: 7px`, `margin-bottom: 6px`. Contains optional `.preview-logo-img` (`20×20px`, `border-radius: 0`) and `.preview-sponsor-name` (`0.8125rem`, weight 500, `var(--muted)`). 455 + 456 + **Headline** — `.preview-headline`: `1rem`, weight 700, `var(--text)`, `line-height: 1.3`. 457 + 458 + **Subline** — `.preview-subline`: `0.8125rem`, `var(--muted)`, `margin-bottom: 10px`. 459 + 460 + **CTA** — `.preview-cta`: `0.8125rem`, weight 700, `var(--orange)`. Always append `↗` character. 461 + 462 + **Caption** — `.preview-label`: `0.75rem`, `var(--muted)`, centered, `margin-top: 10px`. 463 + 464 + --- 465 + 466 + ## Contact Section 467 + 468 + Centered content inside `.contact-inner` (max-width `560px`, `margin: 0 auto`, `text-align: center`). Built on `.section-dark`. 469 + 470 + `.contact-note` — `0.875rem`, `rgba(255,255,255,0.4)`, `margin-top: 16px`. 471 + 472 + --- 473 + 474 + ## Footer 475 + 476 + ### `.footer` 477 + 478 + ```css 479 + padding: 28px 0; 480 + border-top: 1px solid rgba(255,255,255,0.08); 481 + background: var(--maroon-deep); 482 + ``` 483 + 484 + ### `.footer-inner` 485 + 486 + Flex row, `justify-content: space-between`, wraps at small sizes. 487 + 488 + **`.footer-copy`** — `0.8125rem`, `rgba(255,255,255,0.35)`. 489 + 490 + **`.footer-links a`** — `0.8125rem`, `rgba(255,255,255,0.45)`. Hover: `rgba(255,255,255,0.8)`, no underline. 491 + 492 + --- 493 + 494 + ## Responsive Breakpoints 495 + 496 + | Breakpoint | Change | 497 + |---|---| 498 + | `≤ 800px` | `.wizard-grid` collapses to single column | 499 + | `≤ 720px` | `.formats-grid` collapses to single column | 500 + | `≤ 640px` | `.features-grid` collapses to single column; `.hero-stats` collapses to 2 columns | 501 + | `≤ 600px` | `.nav-links` hidden |
+1 -1
workers/gymtracker-ads-api/ACCESS_SETUP.md
··· 61 61 62 62 ## Step 4: Ensure Public API Stays Open 63 63 64 - The public endpoint `GET https://gymtracker.jackhannon.net/api/ads` must **not** require Access. The VT Gym Tracker app fetches ads without auth. 64 + The public endpoint `GET https://gymtracker.jackhannon.net/api/ads` must **not** require Access. The Gym Tracker app fetches ads without auth. 65 65 66 66 - If you only protect `/admin` and `/api/admin`, `/api/ads` stays public. 67 67 - If you protect `/api/*`, add a **Bypass** policy that runs first:
+3 -1
workers/gymtracker-ads-api/README.md
··· 58 58 59 59 **Active logic:** An ad is active only if `active` is true, `now >= start_at` (if set), and `now <= end_at` (if set). If multiple ads are active, the one with the latest `start_at` is returned. 60 60 61 - Also see [ad-config-schema.md](https://github.com/Hann8n/VTGymTracker/blob/main/docs/ad-config-schema.md) in the VT Gym Tracker repo. 61 + Also see [ad-config-schema.md](https://github.com/Hann8n/VTGymTracker/blob/main/docs/ad-config-schema.md) in the Gym Tracker repo. 62 62 63 63 ## Seed First Config 64 64 ··· 79 79 ``` 80 80 81 81 **Admin UI:** Visit [https://gymtracker.jackhannon.net/admin](https://gymtracker.jackhannon.net/admin) — sign in with Cloudflare Access (Google or your configured provider). See [ACCESS_SETUP.md](ACCESS_SETUP.md) to configure. 82 + 83 + **Ads landing page:** Public sales page with mockup wizard at [/ads](https://gymtracker.jackhannon.net/ads). Local dev: run `npx wrangler dev` from this directory, then open http://localhost:8787/ads (port may vary — check wrangler output).
workers/gymtracker-ads-api/public/logo.png

This is a binary file and will not be displayed.

+512 -504
workers/gymtracker-ads-api/src/admin-html.ts
··· 7 7 <title>Gym Tracker Ads Admin</title> 8 8 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css"> 9 9 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/themes/dark.css"> 10 - <style>@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap');</style> 10 + <style> 11 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 12 + :root { 13 + --bg: #0d0d0d; 14 + --surface: #141414; 15 + --border: #262626; 16 + --text: #e5e5e5; 17 + --muted: #737373; 18 + --accent: #14b8a6; 19 + --accent-filled: #0d9488; 20 + --green: #22c55e; 21 + --red: #ef4444; 22 + --yellow: #eab308; 23 + --font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 24 + --font-mono: ui-monospace, 'Cascadia Code', 'SF Mono', Menlo, Consolas, monospace; 25 + --cta-orange: #E87722; 26 + --text-xs: 11px; 27 + --text-sm: 12px; 28 + --text-base: 14px; 29 + --text-md: 15px; 30 + --text-lg: 16px; 31 + --text-xl: 17px; 32 + } 33 + html { background: var(--bg); color: var(--text); font-family: var(--font); font-size: var(--text-base); line-height: 1.5; -webkit-text-size-adjust: 100%; } 34 + body { height: 100vh; margin: 0; overflow: hidden; min-width: 0; -webkit-tap-highlight-color: transparent; } 35 + 36 + .dashboard-wrap { 37 + display: grid; 38 + grid-template-rows: 40px 1fr; 39 + height: 100vh; 40 + min-height: 0; 41 + } 42 + .topbar { 43 + grid-column: 1 / -1; 44 + display: flex; 45 + align-items: center; 46 + gap: 12px; 47 + padding: 0 12px; 48 + min-height: 40px; 49 + height: 40px; 50 + border-bottom: 1px solid var(--border); 51 + background: var(--surface); 52 + min-width: 0; 53 + } 54 + .topbar-home { 55 + font-size: var(--text-sm); font-weight: 600; color: var(--muted); text-decoration: none; white-space: nowrap; 56 + touch-action: manipulation; 57 + } 58 + .topbar-home:hover { color: var(--accent); } 59 + .topbar-center { display: flex; align-items: center; gap: 6px; flex: 1; justify-content: center; min-width: 0; } 60 + .topbar-title { font-size: var(--text-sm); font-weight: 500; white-space: nowrap; } 61 + .topbar-sep { color: var(--border); flex-shrink: 0; opacity: 0.7; } 62 + .topbar-sub { color: var(--muted); font-size: var(--text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 63 + .topbar-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } 64 + .topbar-refresh { 65 + padding: 4px 8px; font-size: 14px; line-height: 1; 66 + border: 1px solid var(--border); background: transparent; color: var(--muted); 67 + cursor: pointer; 68 + } 69 + .topbar-refresh:hover { color: var(--accent); border-color: var(--accent); } 70 + .status { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } 71 + .dot { width: 6px; height: 6px; background: var(--border); flex-shrink: 0; } 72 + .dot.on { background: var(--green); } 73 + .dot.err { background: var(--red); } 74 + .status-text { color: var(--muted); font-size: var(--text-xs); } 75 + .status-text.status-ok { color: var(--green); } 76 + .status-text.status-err { color: var(--red); } 77 + 78 + .kv { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; } 79 + .kv-k { color: var(--muted); white-space: nowrap; } 80 + .kv-v { color: var(--accent); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; } 81 + 82 + .main { display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden; min-width: 0; padding: 16px 20px; } 83 + .schedule-page { flex: 1; min-height: 0; } 84 + .schedule-grid { display: grid; grid-template-columns: 1fr; gap: 20px; } 85 + .schedule-calendar { min-width: 0; } 86 + .schedule-ads { min-width: 0; display: flex; flex-direction: column; gap: 12px; } 87 + 88 + .overview-status-banner { 89 + padding: 12px 16px; 90 + margin: 0 0 16px; 91 + border: 1px solid var(--border); 92 + display: flex; 93 + align-items: center; 94 + gap: 12px; 95 + background: var(--surface); 96 + } 97 + .overview-status-banner.operational { border-left: 4px solid var(--green); } 98 + .overview-status-banner.loading { border-left: 4px solid var(--muted); } 99 + .overview-status-banner.error { border-left: 4px solid var(--red); } 100 + .status-banner-dot { width: 8px; height: 8px; flex-shrink: 0; } 101 + .status-banner-dot.ok { background: var(--green); } 102 + .status-banner-dot.pending { background: var(--muted); } 103 + .status-banner-dot.err { background: var(--red); } 104 + .status-banner-text { font-size: var(--text-sm); font-weight: 500; } 105 + .status-banner-sub { color: var(--muted); font-size: var(--text-xs); margin-top: 2px; } 106 + .btn-as-link { 107 + padding: 7px 12px; 108 + font-size: var(--text-sm); 109 + font-family: var(--font); 110 + border: 1px solid var(--accent); 111 + color: var(--accent); 112 + background: transparent; 113 + cursor: pointer; 114 + width: auto; 115 + transition: color 0.1s, border-color 0.1s, background 0.1s; 116 + } 117 + .btn-as-link:hover { background: var(--accent-filled); color: #fff; } 118 + 119 + .form-overlay { 120 + position: fixed; inset: 0; z-index: 100; 121 + display: flex; align-items: center; justify-content: center; 122 + padding: 16px; 123 + } 124 + .form-overlay[hidden] { display: none; } 125 + .form-overlay-backdrop { 126 + position: absolute; inset: 0; 127 + background: rgba(0,0,0,0.6); 128 + cursor: pointer; 129 + } 130 + .form-overlay-panel { 131 + position: relative; 132 + background: var(--surface); 133 + border: 1px solid var(--border); 134 + max-width: 900px; 135 + width: 100%; 136 + max-height: 90vh; 137 + overflow: hidden; 138 + display: flex; 139 + flex-direction: column; 140 + } 141 + .form-overlay-header { 142 + display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; 143 + padding: 12px 16px; 144 + border-bottom: 1px solid var(--border); 145 + flex-shrink: 0; 146 + } 147 + .form-overlay-header h2 { font-size: var(--text-md); font-weight: 500; margin: 0; min-width: 0; } 148 + .active-inactive-toggle.active-inactive-header { justify-self: center; width: auto; min-width: 180px; margin-top: 0; } 149 + .form-overlay-header > button { 150 + padding: 4px 8px; 151 + font-size: 18px; 152 + line-height: 1; 153 + border: none; 154 + background: none; 155 + color: var(--muted); 156 + cursor: pointer; 157 + justify-self: end; 158 + } 159 + .form-overlay-header > button:hover { color: var(--text); } 160 + .form-overlay-body { 161 + overflow-y: auto; 162 + scrollbar-gutter: stable; 163 + padding: 16px 12px; 164 + flex: 1; 165 + min-height: 0; 166 + } 167 + .form-overlay-footer { 168 + flex-shrink: 0; 169 + padding: 16px 12px; 170 + border-top: 1px solid var(--border); 171 + background: var(--surface); 172 + } 173 + 174 + .admin-section { display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px; min-width: 0; } 175 + 176 + .admin-form { display: flex; flex-direction: column; gap: 24px; } 177 + .admin-form .group { margin-bottom: 0; } 178 + .group { display: flex; flex-direction: column; gap: 12px; margin-bottom: 12px; } 179 + .group-toggle { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 0 0 6px; cursor: pointer; color: inherit; font: inherit; } 180 + .group-toggle .group-label { margin-bottom: 0; } 181 + .group-toggle-icon { font-size: var(--text-xs); color: var(--muted); } 182 + .group-label { font-size: var(--text-xs); color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 0; } 183 + .field { display: flex; flex-direction: column; gap: 4px; } 184 + .field-label { color: var(--muted); font-size: var(--text-xs); margin: 0; } 185 + .field-error { display: block; font-size: var(--text-xs); color: var(--red); margin: 0; min-height: 0; } 186 + .checkbox-label { display: flex; align-items: center; gap: 8px; color: var(--text); font-size: var(--text-sm); cursor: pointer; } 187 + .checkbox-label input { width: auto; margin: 0; } 188 + 189 + input, select { 190 + width: 100%; 191 + min-width: 0; 192 + min-height: 40px; 193 + background: var(--bg); 194 + border: 1px solid var(--border); 195 + color: var(--text); 196 + font-family: var(--font-mono); 197 + font-size: var(--text-base); 198 + padding: 10px 12px; 199 + outline: none; 200 + transition: border-color 0.15s; 201 + margin-bottom: 0; 202 + } 203 + input:focus, select:focus { border-color: var(--accent); } 204 + input.input-error { border-color: var(--red); } 205 + 206 + button { 207 + font-family: var(--font); 208 + font-size: var(--text-sm); 209 + transition: color 0.15s, border-color 0.15s, background 0.15s; 210 + padding: 7px 12px; 211 + cursor: pointer; 212 + border: 1px solid var(--border); 213 + background: transparent; 214 + color: var(--muted); 215 + transition: color 0.1s, border-color 0.1s, background 0.1s; 216 + touch-action: manipulation; 217 + } 218 + button:hover:not(:disabled) { color: var(--text); border-color: var(--text); } 219 + button.primary { border-color: var(--accent); color: var(--accent); } 220 + button.primary:hover:not(:disabled) { background: var(--accent-filled); color: #fff; } 221 + button.delete-btn { border-color: var(--red); color: var(--red); margin-left: auto; } 222 + button.delete-btn:hover:not(:disabled) { background: var(--red); color: #fff; } 223 + .date-presets { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; } 224 + .date-presets button { width: auto; } 225 + .end-at-wrap { margin-top: 6px; } 226 + #imageUrlWrap { 227 + overflow: hidden; 228 + max-height: 100px; 229 + opacity: 1; 230 + transition: opacity 0.2s ease, max-height 0.25s ease; 231 + } 232 + #imageUrlWrap.image-url-hidden { 233 + opacity: 0; 234 + max-height: 0; 235 + pointer-events: none; 236 + } 237 + .clear-dates-wrap { margin-top: 6px; } 238 + .clear-dates-btn { padding: 2px 0; font-size: var(--text-xs); color: var(--muted); border: none; background: none; cursor: pointer; text-decoration: underline; } 239 + .clear-dates-btn:hover { color: var(--text); } 240 + 241 + .form-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 0; padding: 0; position: relative; } 242 + .form-actions .delete-btn { margin-left: 0; } 243 + .form-actions-right { display: flex; align-items: center; gap: 12px; margin-left: auto; } 244 + .active-inactive-toggle { 245 + display: flex; width: 100%; margin-top: 12px; border: 1px solid var(--border); 246 + } 247 + .active-inactive-btn { 248 + flex: 1; padding: 10px 16px; font-size: var(--text-sm); font-weight: 500; 249 + border: none; background: transparent; color: var(--muted); 250 + cursor: pointer; transition: color 0.15s, background 0.15s; 251 + } 252 + .active-inactive-btn:first-child { border-right: none; } 253 + .active-inactive-btn:hover { color: var(--text); } 254 + .active-inactive-btn.active { background: rgba(34,197,94,0.12); color: var(--green); } 255 + .active-inactive-btn:last-child.active { background: rgba(115,115,115,0.15); color: var(--muted); } 256 + 257 + .tier-toggle { 258 + display: flex; width: 100%; margin-top: 12px; border: 1px solid var(--border); 259 + } 260 + .schedule-group { gap: 8px; } 261 + .schedule-group .date-presets { margin-top: 8px; margin-bottom: 0; } 262 + .tier-btn { 263 + flex: 1; padding: 10px 16px; font-size: var(--text-sm); font-weight: 500; 264 + border: none; background: transparent; color: var(--muted); 265 + cursor: pointer; transition: color 0.15s, background 0.15s; 266 + } 267 + .tier-btn:not(:last-child) { border-right: none; } 268 + .tier-btn:hover { color: var(--text); } 269 + .tier-btn.active { background: rgba(20,184,166,0.12); color: var(--accent); } 270 + .unsaved-indicator { font-size: var(--text-xs); color: var(--yellow); position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; } 271 + .save-status { font-size: var(--text-xs); color: var(--muted); position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; } 272 + .save-status.status-ok { color: var(--green); } 273 + .save-status.status-err { color: var(--red); } 274 + 275 + #adCards { display: flex; flex-direction: column; gap: 4px; } 276 + .ad-cards { 277 + display: grid; 278 + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); 279 + gap: 10px; 280 + min-width: 0; 281 + } 282 + .ad-cards-group { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; } 283 + .main-toolbar { 284 + display: flex; align-items: center; gap: 12px; flex-wrap: wrap; 285 + margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border); 286 + } 287 + .toolbar-search { flex: 1; min-width: 180px; max-width: 360px; margin: 0; } 288 + .toolbar-filters { display: flex; flex-wrap: wrap; gap: 4px; } 289 + .ad-cards-group-new .ad-card-wrap { max-width: 160px; } 290 + .new-ad-card { justify-content: center; align-items: center; text-align: center; color: var(--muted); min-height: 80px; padding-right: 12px; } 291 + .new-ad-card:hover { border-color: var(--accent); color: var(--accent); background: rgba(20,184,166,0.08); } 292 + .new-ad-plus { font-size: 20px; line-height: 1; } 293 + .new-ad-label { font-size: var(--text-xs); font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; } 294 + .ads-status-filters { display: flex; flex-wrap: wrap; gap: 4px; } 295 + .status-filter { padding: 4px 8px; font-size: var(--text-xs); } 296 + .status-filter.active { border-color: var(--accent); color: var(--accent); } 297 + .ad-cards-group:last-child { margin-bottom: 0; } 298 + .ad-cards-group-label { font-size: var(--text-xs); color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 2px; } 299 + .ad-card-wrap { position: relative; min-width: 0; height: 100%; } 300 + .ad-card-wrap .ad-card { position: relative; min-width: 0; min-height: 80px; width: 100%; height: 100%; } 301 + .ad-card-action { 302 + position: absolute; top: 8px; right: 8px; 303 + padding: 8px; display: flex; align-items: center; justify-content: center; 304 + z-index: 1; flex-shrink: 0; 305 + } 306 + .ad-card-action-icon-wrap { 307 + position: relative; display: inline-flex; width: 18px; height: 18px; 308 + align-items: center; justify-content: center; flex-shrink: 0; 309 + } 310 + .ad-card-action-icon { 311 + font-size: 16px; line-height: 1; transition: opacity 0.15s; 312 + display: flex; align-items: center; justify-content: center; 313 + width: 18px; height: 18px; 314 + } 315 + .ad-card-action-icon-action { position: absolute; opacity: 0; } 316 + .ad-card-action:hover .ad-card-action-icon-state { opacity: 0; } 317 + .ad-card-action:hover .ad-card-action-icon-action { opacity: 1; } 318 + .ad-card-action-live { color: var(--green); background: rgba(34,197,94,0.15); border-color: rgba(34,197,94,0.4); } 319 + .ad-card-action-paused { color: var(--yellow); background: rgba(234,179,8,0.15); border-color: rgba(234,179,8,0.4); } 320 + .ad-card-action-live:hover, 321 + .ad-card-action-live:focus-visible { 322 + background: rgba(234,179,8,0.2) !important; border-color: var(--yellow) !important; color: var(--yellow) !important; 323 + } 324 + .ad-card-action-paused:hover, 325 + .ad-card-action-paused:focus-visible { 326 + background: rgba(34,197,94,0.2) !important; border-color: var(--green) !important; color: var(--green) !important; 327 + } 328 + .ad-card { 329 + display: flex; flex-direction: column; align-items: flex-start; gap: 4px; 330 + padding: clamp(10px, 2.5vw, 14px) clamp(12px, 3vw, 16px); 331 + padding-right: 56px; border: 1px solid var(--border); background: var(--surface); 332 + color: var(--text); font: inherit; text-align: left; cursor: pointer; 333 + transition: border-color 0.15s, background 0.15s; box-sizing: border-box; 334 + min-height: clamp(72px, 18vw, 88px); 335 + } 336 + .ad-card:hover { border-color: var(--text); } 337 + .ad-card.selected { border-color: var(--accent); background: rgba(20,184,166,0.08); } 338 + .ad-card-head { font-weight: 500; font-size: var(--text-sm); line-height: 1.4; word-break: break-word; } 339 + .ad-card-dates { font-size: var(--text-xs); color: var(--muted); line-height: 1.4; } 340 + .ad-card-stats { font-size: var(--text-xs); color: var(--green); display: block; margin-top: 2px; line-height: 1.4; } 341 + .ad-card-tier { font-size: var(--text-xs); color: var(--muted); text-transform: uppercase; line-height: 1.4; } 342 + .ad-kpi-box { margin-bottom: 16px; padding: 10px 12px; background: var(--bg); border: 1px solid var(--border); } 343 + .ad-kpi-box .group-label { margin-bottom: 8px; } 344 + .kpi-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 16px; } 345 + .kpi-item { display: flex; flex-direction: column; gap: 2px; } 346 + .kpi-label { font-size: var(--text-xs); color: var(--muted); text-transform: uppercase; } 347 + .kpi-value { font-size: var(--text-sm); font-weight: 500; color: var(--green); } 348 + .chip { font-size: var(--text-xs); font-weight: 500; padding: 2px 6px; border: 1px solid transparent; line-height: 1.5; } 349 + .chip-live { color: var(--green); background: rgba(34,197,94,0.15); border-color: rgba(34,197,94,0.3); } 350 + .chip-scheduled { color: var(--accent); background: rgba(20,184,166,0.15); border-color: rgba(20,184,166,0.3); } 351 + .chip-ended { color: var(--muted); background: rgba(115,115,115,0.1); opacity: 0.9; } 352 + .chip-paused { color: var(--yellow); background: rgba(234,179,8,0.15); border-color: rgba(234,179,8,0.3); } 353 + 354 + #adCards.loading, #calendarWrap.loading { opacity: 0.6; pointer-events: none; } 355 + .calendar-empty { margin-top: 16px; padding: 24px; border: 1px dashed var(--border); background: var(--surface); text-align: center; max-width: 240px; } 356 + .calendar-empty-text { color: var(--muted); font-size: var(--text-sm); margin: 0; } 357 + .calendar-wrap { margin-top: 16px; border: 1px solid var(--border); background: var(--surface); padding: 18px 22px; width: 100%; min-width: 340px; max-width: 100%; } 358 + .calendar-header { display: flex; align-items: center; justify-content: center; gap: 20px; margin-bottom: 14px; } 359 + .cal-month-label { font-size: var(--text-md); font-weight: 500; color: var(--text); margin: 0; } 360 + .cal-nav { padding: 4px 10px; font-size: var(--text-xs); background: transparent; border: 1px solid var(--border); color: var(--muted); cursor: pointer; } 361 + .cal-nav.cal-today { margin-left: 8px; } 362 + .cal-nav:hover { color: var(--text); border-color: var(--text); } 363 + .calendar-grid { display: flex; flex-direction: column; gap: 0; } 364 + .cal-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); font-size: var(--text-xs); color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; padding-bottom: 8px; border-bottom: 1px solid var(--border); } 365 + .cal-weekdays span { text-align: center; } 366 + .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: var(--border); width: 100%; } 367 + .cal-cell { min-height: 80px; height: auto; padding: 10px; background: var(--surface); display: flex; flex-direction: column; font-size: var(--text-xs); } 368 + .cal-cell.other-month { background: var(--bg); } 369 + .cal-cell.other-month .cal-cell-num { color: var(--muted); opacity: 0.6; } 370 + .cal-cell.today { outline: 1px solid var(--accent); outline-offset: -1px; z-index: 1; } 371 + .cal-cell.today .cal-cell-num { color: var(--accent); font-weight: 500; } 372 + .cal-cell.has-conflict { outline: 1px dashed var(--yellow); outline-offset: -1px; } 373 + .cal-cell-num { font-size: var(--text-sm); color: var(--text); margin-bottom: 4px; } 374 + .cal-cell-ads { display: flex; flex-wrap: wrap; gap: 2px; align-content: flex-start; overflow: hidden; } 375 + .cal-ad-pill { font-size: var(--text-xs); padding: 5px 8px; min-height: 28px; border: none; cursor: pointer; font-family: var(--font); text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } 376 + .cal-ad-pill.chip-live { background: rgba(34,197,94,0.2); color: var(--green); } 377 + .cal-ad-pill.chip-scheduled { background: rgba(20,184,166,0.2); color: var(--accent); } 378 + .cal-ad-pill.chip-ended { background: rgba(102,102,102,0.2); color: var(--muted); } 379 + .cal-ad-pill.chip-paused { background: rgba(251,191,36,0.2); color: var(--yellow); } 380 + .cal-ad-pill:hover { opacity: 0.9; } 381 + .cal-ad-pill.selected { outline: 1px solid var(--text); outline-offset: 1px; } 382 + .cal-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); font-size: var(--text-xs); color: var(--muted); } 383 + .cal-legend-dot { display: inline-block; width: 6px; height: 6px; margin-right: 4px; vertical-align: middle; } 384 + .cal-legend-dot.live { background: var(--green); } 385 + .cal-legend-dot.scheduled { background: var(--accent); } 386 + .cal-legend-dot.ended { background: var(--muted); } 387 + .cal-legend-dot.paused { background: var(--yellow); } 388 + 389 + .admin-editor-grid { display: grid; grid-template-columns: 1fr minmax(280px, 393px); gap: 20px; min-width: 0; } 390 + .preview-pane { position: sticky; top: 0; align-self: start; padding-top: 0; } 391 + .preview-pane > :first-child { margin-top: 0; } 392 + .id-version-row { display: flex; flex-direction: row; gap: 8px; } 393 + .id-version-row input:first-child { flex: 1; min-width: 0; } 394 + .id-version-row .version-input { width: 64px; min-width: 64px; flex-shrink: 0; } 395 + .id-version-header { min-width: 0; max-width: 320px; justify-self: center; } 396 + .id-version-header input { min-height: 36px; padding: 6px 10px; font-size: var(--text-sm); } 397 + .ad-preview { border: 1px solid var(--border); padding: 0; background: var(--surface); min-height: 8rem; overflow: hidden; } 398 + /* Matches Swift AdView + SponsorSectionHeader layout */ 399 + .preview { display: flex; flex-direction: column; align-items: flex-start; text-align: left; width: 100%; max-width: 100%; } 400 + .preview-header { 401 + display: flex; align-items: center; gap: 8px; width: 100%; 402 + padding: 0 16px 8px; border-bottom: 1px solid var(--border); margin-bottom: 0; 403 + } 404 + .preview-header-logo { width: 20px; height: 20px; object-fit: contain; border-radius: 4px; flex-shrink: 0; } 405 + .preview-header-sponsor { font-size: var(--text-sm); font-weight: 500; color: var(--text); flex: 1; } 406 + .preview-header-sponsored { font-size: var(--text-xs); color: var(--muted); } 407 + .preview.preview-text { padding: 16px; gap: 12px; } 408 + .preview.preview-text .preview-header { padding: 0 0 8px; margin-bottom: 4px; } 409 + .preview.preview-banner .preview-copy-block, 410 + .preview.preview-feature .preview-copy-block { padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; width: 100%; } 411 + .preview.preview-banner .preview-header, 412 + .preview.preview-feature .preview-header { padding: 12px 16px 8px; } 413 + .preview-copy { display: flex; flex-direction: column; gap: 10px; width: 100%; } 414 + .preview-headline { font-size: var(--text-lg); font-weight: 600; line-height: 1.3; color: var(--text); } 415 + .preview-subline { font-size: var(--text-sm); color: var(--muted); line-height: 1.4; } 416 + .preview-cta-wrap { 417 + display: flex; align-items: center; justify-content: center; gap: 6px; 418 + width: 100%; padding: 10px; 419 + color: var(--cta-orange); background: rgba(232, 119, 34, 0.12); 420 + font-size: var(--text-sm); font-weight: 500; cursor: default; border: none; 421 + border-radius: 8px; box-sizing: border-box; 422 + } 423 + .preview-cta-arrow { font-size: 12px; opacity: 0.9; } 424 + .preview-img-wrap { width: 100%; overflow: hidden; position: relative; background: var(--border); } 425 + .preview-img { width: 100%; height: 100%; object-fit: cover; display: block; } 426 + .preview-img-wrap.preview-img-error { background: var(--border); } 427 + .preview-img-wrap.preview-img-error .preview-img { display: none; } 428 + .preview-img-placeholder { width: 100%; background: var(--border); display: flex; align-items: center; justify-content: center; font-size: var(--text-sm); color: var(--muted); } 429 + 430 + ::-webkit-scrollbar { width: 4px; height: 4px; } 431 + ::-webkit-scrollbar-thumb { background: var(--border); } 432 + 433 + .flatpickr-calendar { z-index: 99999; } 434 + .flatpickr-day.selected, .flatpickr-day.startRange, .flatpickr-day.endRange { background: var(--accent) !important; border-color: var(--accent) !important; } 435 + .flatpickr-day:hover { background: var(--border) !important; border-color: var(--border) !important; } 436 + #start_at[readonly], #end_at[readonly] { cursor: pointer; } 437 + 438 + @media (min-width: 900px) { 439 + .schedule-grid { grid-template-columns: 1fr minmax(420px, 560px); } 440 + } 441 + @media (min-width: 1200px) { 442 + .schedule-grid { grid-template-columns: 1fr minmax(480px, 640px); } 443 + .cal-cell { min-height: 88px; } 444 + .cal-ad-pill { font-size: var(--text-xs); min-height: 30px; } 445 + } 446 + 447 + @media (max-width: 768px) { 448 + .form-overlay { padding: 0; align-items: stretch; justify-content: stretch; } 449 + .form-overlay-backdrop { display: none; } 450 + .form-overlay-panel { 451 + width: 100%; height: 100%; max-width: none; max-height: none; 452 + border: none; border-radius: 0; 453 + } 454 + .admin-editor-grid { grid-template-columns: 1fr; } 455 + .preview-pane { position: static; } 456 + .calendar-wrap { max-width: 100%; padding: 10px 12px; min-width: 0; } 457 + .cal-cell { min-height: 56px; padding: 6px; } 458 + .cal-cell-num { font-size: var(--text-xs); } 459 + .cal-ad-pill { font-size: var(--text-xs); padding: 4px 6px; min-height: 36px; } 460 + .topbar-refresh { 461 + min-width: 44px; min-height: 44px; padding: 0 12px; 462 + display: flex; align-items: center; justify-content: center; 463 + border: none; border-left: 1px solid var(--border); border-right: 1px solid var(--border); 464 + border-radius: 0; 465 + } 466 + .topbar-refresh:hover { border-left-color: var(--accent); border-right-color: var(--accent); } 467 + .status-filter { min-height: 44px; min-width: 44px; padding: 10px 12px; display: inline-flex; align-items: center; justify-content: center; } 468 + .cal-nav { min-height: 44px; min-width: 44px; padding: 10px; display: inline-flex; align-items: center; justify-content: center; } 469 + .cal-nav.cal-today { min-width: auto; padding: 10px 14px; } 470 + .form-overlay-header { grid-template-columns: 1fr auto; grid-template-rows: auto auto; gap: 12px; } 471 + .form-overlay-header h2 { grid-column: 1; grid-row: 1; } 472 + .form-overlay-header > button[aria-label="Close"] { grid-column: 2; grid-row: 1; min-width: 44px; min-height: 44px; padding: 10px; } 473 + .active-inactive-toggle.active-inactive-header { grid-column: 1 / -1; grid-row: 2; justify-self: stretch; margin-top: 0; } 474 + .form-overlay-header .active-inactive-btn { min-height: 44px; } 475 + } 476 + @media (max-width: 640px) { 477 + :root { 478 + --text-xs: 12px; 479 + --text-sm: 13px; 480 + --text-base: 15px; 481 + --text-md: 16px; 482 + } 483 + .dashboard-wrap { grid-template-rows: 40px 1fr; } 484 + .main { padding: 12px 16px; } 485 + .overview-status-banner { padding: 10px 12px; margin-bottom: 12px; } 486 + .status-banner-text { font-size: var(--text-sm); } 487 + .status-banner-sub { font-size: var(--text-xs); } 488 + .calendar-wrap { margin-top: 12px; min-width: 0; } 489 + .cal-cell { min-height: 50px; padding: 5px; font-size: var(--text-xs); } 490 + .cal-legend { margin-top: 10px; padding-top: 10px; gap: 8px; font-size: var(--text-xs); } 491 + .ad-card { padding: 10px 12px; min-width: 0; padding-right: 52px; } 492 + .ad-card-action { min-width: 44px; min-height: 44px; padding: 10px; } 493 + .ad-card-action-icon-wrap { width: 20px; height: 20px; } 494 + .ad-card-action-icon { font-size: 18px; width: 20px; height: 20px; } 495 + .form-actions, .form-actions-right { gap: 8px; } 496 + .date-presets button { min-height: 44px; min-width: 44px; padding: 10px 14px; } 497 + .tier-btn, .active-inactive-btn { min-height: 44px; } 498 + .btn-as-link { min-height: 44px; padding: 10px 16px; } 499 + } 500 + @media (max-width: 400px) { 501 + .topbar { padding: 0 8px; gap: 8px; } 502 + .topbar-sub { display: none; } 503 + .topbar-sep { display: none; } 504 + .topbar-title { font-size: var(--text-sm); } 505 + .topbar-home { font-size: var(--text-sm); } 506 + .main-toolbar { flex-direction: column; align-items: stretch; } 507 + .toolbar-search { max-width: none; } 508 + .form-actions { flex-direction: column; align-items: stretch; } 509 + .form-actions-right { margin-left: 0; flex-wrap: wrap; } 510 + .form-actions-right button { flex: 1; min-height: 44px; } 511 + .delete-btn { min-height: 44px; } 512 + .new-ad-card { min-height: 88px; } 513 + .ad-cards { grid-template-columns: 1fr; } 514 + } 515 + </style> 11 516 </head> 12 517 <body> 13 518 <div class="dashboard-wrap"> ··· 828 1333 const image_url = d.image_url || null; 829 1334 const logo_url = d.logo_url || null; 830 1335 const usesImageLayout = tier !== 'text'; 831 - const sponsorLine = '<div class="preview-sponsor-line">' + 832 - (logo_url ? '<img src="' + escapeHtml(logo_url) + '" alt="" class="preview-logo" onerror="this.style.display=\\'none\\'">' : '') + 833 - '<span class="preview-sponsor">' + escapeHtml(sponsor) + '</span></div>'; 1336 + const sponsorHeader = '<div class="preview-header">' + 1337 + (logo_url ? '<img src="' + escapeHtml(logo_url) + '" alt="" class="preview-header-logo" onerror="this.style.display=\\'none\\'">' : '') + 1338 + '<span class="preview-header-sponsor">' + escapeHtml(sponsor) + '</span>' + 1339 + '<span class="preview-header-sponsored">Sponsored</span></div>'; 834 1340 const copyContent = '<div class="preview-copy">' + 835 - sponsorLine + 836 1341 '<strong class="preview-headline">' + escapeHtml(headline) + '</strong>' + 837 - '<span class="preview-subline">' + escapeHtml(subline) + '</span>' + 1342 + (subline ? '<span class="preview-subline">' + escapeHtml(subline) + '</span>' : '') + 838 1343 '</div>'; 839 1344 const ctaBtn = '<div class="preview-cta-wrap"><span class="preview-cta-text">' + escapeHtml(cta) + '</span><span class="preview-cta-arrow">↗</span></div>'; 840 1345 let html = '<div class="preview preview-' + tier + '">'; 1346 + html += sponsorHeader; 841 1347 if (usesImageLayout) { 842 1348 const imgHeight = tier === 'feature' ? 220 : 140; 843 1349 if (image_url) { ··· 1186 1692 updateFilterChips(countByStatus()); 1187 1693 loadSchedule(); 1188 1694 </script> 1189 - 1190 - <style> 1191 - *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 1192 - :root { 1193 - --bg: #0d0d0d; 1194 - --surface: #141414; 1195 - --border: #262626; 1196 - --text: #e5e5e5; 1197 - --muted: #737373; 1198 - --accent: #14b8a6; 1199 - --accent-filled: #0d9488; 1200 - --green: #22c55e; 1201 - --red: #ef4444; 1202 - --yellow: #eab308; 1203 - --font: 'DM Sans', system-ui, sans-serif; 1204 - --font-mono: 'JetBrains Mono', monospace; 1205 - --cta-orange: #E87722; 1206 - --text-xs: 11px; 1207 - --text-sm: 12px; 1208 - --text-base: 14px; 1209 - --text-md: 15px; 1210 - --text-lg: 16px; 1211 - --text-xl: 17px; 1212 - } 1213 - html { background: var(--bg); color: var(--text); font-family: var(--font); font-size: var(--text-base); line-height: 1.5; -webkit-text-size-adjust: 100%; } 1214 - body { height: 100vh; margin: 0; overflow: hidden; min-width: 0; -webkit-tap-highlight-color: transparent; } 1215 - 1216 - .dashboard-wrap { 1217 - display: grid; 1218 - grid-template-rows: 40px 1fr; 1219 - height: 100vh; 1220 - min-height: 0; 1221 - } 1222 - .topbar { 1223 - grid-column: 1 / -1; 1224 - display: flex; 1225 - align-items: center; 1226 - gap: 12px; 1227 - padding: 0 12px; 1228 - min-height: 40px; 1229 - height: 40px; 1230 - border-bottom: 1px solid var(--border); 1231 - background: var(--surface); 1232 - min-width: 0; 1233 - } 1234 - .topbar-home { 1235 - font-size: var(--text-sm); font-weight: 600; color: var(--muted); text-decoration: none; white-space: nowrap; 1236 - touch-action: manipulation; 1237 - } 1238 - .topbar-home:hover { color: var(--accent); } 1239 - .topbar-center { display: flex; align-items: center; gap: 6px; flex: 1; justify-content: center; min-width: 0; } 1240 - .topbar-title { font-size: var(--text-sm); font-weight: 500; white-space: nowrap; } 1241 - .topbar-sep { color: var(--border); flex-shrink: 0; opacity: 0.7; } 1242 - .topbar-sub { color: var(--muted); font-size: var(--text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 1243 - .topbar-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } 1244 - .topbar-refresh { 1245 - padding: 4px 8px; font-size: 14px; line-height: 1; 1246 - border: 1px solid var(--border); background: transparent; color: var(--muted); 1247 - cursor: pointer; 1248 - } 1249 - .topbar-refresh:hover { color: var(--accent); border-color: var(--accent); } 1250 - .status { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } 1251 - .dot { width: 6px; height: 6px; background: var(--border); flex-shrink: 0; } 1252 - .dot.on { background: var(--green); } 1253 - .dot.err { background: var(--red); } 1254 - .status-text { color: var(--muted); font-size: var(--text-xs); } 1255 - .status-text.status-ok { color: var(--green); } 1256 - .status-text.status-err { color: var(--red); } 1257 - 1258 - .kv { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; } 1259 - .kv-k { color: var(--muted); white-space: nowrap; } 1260 - .kv-v { color: var(--accent); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; } 1261 - 1262 - .main { display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden; min-width: 0; padding: 16px 20px; } 1263 - .schedule-page { flex: 1; min-height: 0; } 1264 - .schedule-grid { display: grid; grid-template-columns: 1fr; gap: 20px; } 1265 - .schedule-calendar { min-width: 0; } 1266 - .schedule-ads { min-width: 0; display: flex; flex-direction: column; gap: 12px; } 1267 - 1268 - .overview-status-banner { 1269 - padding: 12px 16px; 1270 - margin: 0 0 16px; 1271 - border: 1px solid var(--border); 1272 - display: flex; 1273 - align-items: center; 1274 - gap: 12px; 1275 - background: var(--surface); 1276 - } 1277 - .overview-status-banner.operational { border-left: 4px solid var(--green); } 1278 - .overview-status-banner.loading { border-left: 4px solid var(--muted); } 1279 - .overview-status-banner.error { border-left: 4px solid var(--red); } 1280 - .status-banner-dot { width: 8px; height: 8px; flex-shrink: 0; } 1281 - .status-banner-dot.ok { background: var(--green); } 1282 - .status-banner-dot.pending { background: var(--muted); } 1283 - .status-banner-dot.err { background: var(--red); } 1284 - .status-banner-text { font-size: var(--text-sm); font-weight: 500; } 1285 - .status-banner-sub { color: var(--muted); font-size: var(--text-xs); margin-top: 2px; } 1286 - .btn-as-link { 1287 - padding: 7px 12px; 1288 - font-size: var(--text-sm); 1289 - font-family: var(--font); 1290 - border: 1px solid var(--accent); 1291 - color: var(--accent); 1292 - background: transparent; 1293 - cursor: pointer; 1294 - width: auto; 1295 - transition: color 0.1s, border-color 0.1s, background 0.1s; 1296 - } 1297 - .btn-as-link:hover { background: var(--accent-filled); color: #fff; } 1298 - 1299 - .form-overlay { 1300 - position: fixed; inset: 0; z-index: 100; 1301 - display: flex; align-items: center; justify-content: center; 1302 - padding: 16px; 1303 - } 1304 - .form-overlay[hidden] { display: none; } 1305 - .form-overlay-backdrop { 1306 - position: absolute; inset: 0; 1307 - background: rgba(0,0,0,0.6); 1308 - cursor: pointer; 1309 - } 1310 - .form-overlay-panel { 1311 - position: relative; 1312 - background: var(--surface); 1313 - border: 1px solid var(--border); 1314 - max-width: 900px; 1315 - width: 100%; 1316 - max-height: 90vh; 1317 - overflow: hidden; 1318 - display: flex; 1319 - flex-direction: column; 1320 - } 1321 - .form-overlay-header { 1322 - display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; 1323 - padding: 12px 16px; 1324 - border-bottom: 1px solid var(--border); 1325 - flex-shrink: 0; 1326 - } 1327 - .form-overlay-header h2 { font-size: var(--text-md); font-weight: 500; margin: 0; min-width: 0; } 1328 - .active-inactive-toggle.active-inactive-header { justify-self: center; width: auto; min-width: 180px; margin-top: 0; } 1329 - .form-overlay-header > button { 1330 - padding: 4px 8px; 1331 - font-size: 18px; 1332 - line-height: 1; 1333 - border: none; 1334 - background: none; 1335 - color: var(--muted); 1336 - cursor: pointer; 1337 - justify-self: end; 1338 - } 1339 - .form-overlay-header > button:hover { color: var(--text); } 1340 - .form-overlay-body { 1341 - overflow-y: auto; 1342 - scrollbar-gutter: stable; 1343 - padding: 16px 12px; 1344 - flex: 1; 1345 - min-height: 0; 1346 - } 1347 - .form-overlay-footer { 1348 - flex-shrink: 0; 1349 - padding: 16px 12px; 1350 - border-top: 1px solid var(--border); 1351 - background: var(--surface); 1352 - } 1353 - 1354 - .admin-section { display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px; min-width: 0; } 1355 - 1356 - .admin-form { display: flex; flex-direction: column; gap: 24px; } 1357 - .admin-form .group { margin-bottom: 0; } 1358 - .group { display: flex; flex-direction: column; gap: 12px; margin-bottom: 12px; } 1359 - .group-toggle { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 0 0 6px; cursor: pointer; color: inherit; font: inherit; } 1360 - .group-toggle .group-label { margin-bottom: 0; } 1361 - .group-toggle-icon { font-size: var(--text-xs); color: var(--muted); } 1362 - .group-label { font-size: var(--text-xs); color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 0; } 1363 - .field { display: flex; flex-direction: column; gap: 4px; } 1364 - .field-label { color: var(--muted); font-size: var(--text-xs); margin: 0; } 1365 - .field-error { display: block; font-size: var(--text-xs); color: var(--red); margin: 0; min-height: 0; } 1366 - .checkbox-label { display: flex; align-items: center; gap: 8px; color: var(--text); font-size: var(--text-sm); cursor: pointer; } 1367 - .checkbox-label input { width: auto; margin: 0; } 1368 - 1369 - input, select { 1370 - width: 100%; 1371 - min-width: 0; 1372 - min-height: 40px; 1373 - background: var(--bg); 1374 - border: 1px solid var(--border); 1375 - color: var(--text); 1376 - font-family: var(--font-mono); 1377 - font-size: var(--text-base); 1378 - padding: 10px 12px; 1379 - outline: none; 1380 - transition: border-color 0.15s; 1381 - margin-bottom: 0; 1382 - } 1383 - input:focus, select:focus { border-color: var(--accent); } 1384 - input.input-error { border-color: var(--red); } 1385 - 1386 - button { 1387 - font-family: var(--font); 1388 - font-size: var(--text-sm); 1389 - transition: color 0.15s, border-color 0.15s, background 0.15s; 1390 - padding: 7px 12px; 1391 - cursor: pointer; 1392 - border: 1px solid var(--border); 1393 - background: transparent; 1394 - color: var(--muted); 1395 - transition: color 0.1s, border-color 0.1s, background 0.1s; 1396 - touch-action: manipulation; 1397 - } 1398 - button:hover:not(:disabled) { color: var(--text); border-color: var(--text); } 1399 - button.primary { border-color: var(--accent); color: var(--accent); } 1400 - button.primary:hover:not(:disabled) { background: var(--accent-filled); color: #fff; } 1401 - button.delete-btn { border-color: var(--red); color: var(--red); margin-left: auto; } 1402 - button.delete-btn:hover:not(:disabled) { background: var(--red); color: #fff; } 1403 - .date-presets { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; } 1404 - .date-presets button { width: auto; } 1405 - .end-at-wrap { margin-top: 6px; } 1406 - #imageUrlWrap { 1407 - overflow: hidden; 1408 - max-height: 100px; 1409 - opacity: 1; 1410 - transition: opacity 0.2s ease, max-height 0.25s ease; 1411 - } 1412 - #imageUrlWrap.image-url-hidden { 1413 - opacity: 0; 1414 - max-height: 0; 1415 - pointer-events: none; 1416 - } 1417 - .clear-dates-wrap { margin-top: 6px; } 1418 - .clear-dates-btn { padding: 2px 0; font-size: var(--text-xs); color: var(--muted); border: none; background: none; cursor: pointer; text-decoration: underline; } 1419 - .clear-dates-btn:hover { color: var(--text); } 1420 - 1421 - .form-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 0; padding: 0; position: relative; } 1422 - .form-actions .delete-btn { margin-left: 0; } 1423 - .form-actions-right { display: flex; align-items: center; gap: 12px; margin-left: auto; } 1424 - .active-inactive-toggle { 1425 - display: flex; width: 100%; margin-top: 12px; border: 1px solid var(--border); 1426 - } 1427 - .active-inactive-btn { 1428 - flex: 1; padding: 10px 16px; font-size: var(--text-sm); font-weight: 500; 1429 - border: none; background: transparent; color: var(--muted); 1430 - cursor: pointer; transition: color 0.15s, background 0.15s; 1431 - } 1432 - .active-inactive-btn:first-child { border-right: none; } 1433 - .active-inactive-btn:hover { color: var(--text); } 1434 - .active-inactive-btn.active { background: rgba(34,197,94,0.12); color: var(--green); } 1435 - .active-inactive-btn:last-child.active { background: rgba(115,115,115,0.15); color: var(--muted); } 1436 - 1437 - .tier-toggle { 1438 - display: flex; width: 100%; margin-top: 12px; border: 1px solid var(--border); 1439 - } 1440 - .schedule-group { gap: 8px; } 1441 - .schedule-group .date-presets { margin-top: 8px; margin-bottom: 0; } 1442 - .tier-btn { 1443 - flex: 1; padding: 10px 16px; font-size: var(--text-sm); font-weight: 500; 1444 - border: none; background: transparent; color: var(--muted); 1445 - cursor: pointer; transition: color 0.15s, background 0.15s; 1446 - } 1447 - .tier-btn:not(:last-child) { border-right: none; } 1448 - .tier-btn:hover { color: var(--text); } 1449 - .tier-btn.active { background: rgba(20,184,166,0.12); color: var(--accent); } 1450 - .unsaved-indicator { font-size: var(--text-xs); color: var(--yellow); position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; } 1451 - .save-status { font-size: var(--text-xs); color: var(--muted); position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; } 1452 - .save-status.status-ok { color: var(--green); } 1453 - .save-status.status-err { color: var(--red); } 1454 - 1455 - #adCards { display: flex; flex-direction: column; gap: 4px; } 1456 - .ad-cards { 1457 - display: grid; 1458 - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); 1459 - gap: 10px; 1460 - min-width: 0; 1461 - } 1462 - .ad-cards-group { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; } 1463 - .main-toolbar { 1464 - display: flex; align-items: center; gap: 12px; flex-wrap: wrap; 1465 - margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border); 1466 - } 1467 - .toolbar-search { flex: 1; min-width: 180px; max-width: 360px; margin: 0; } 1468 - .toolbar-filters { display: flex; flex-wrap: wrap; gap: 4px; } 1469 - .ad-cards-group-new .ad-card-wrap { max-width: 160px; } 1470 - .new-ad-card { justify-content: center; align-items: center; text-align: center; color: var(--muted); min-height: 80px; padding-right: 12px; } 1471 - .new-ad-card:hover { border-color: var(--accent); color: var(--accent); background: rgba(20,184,166,0.08); } 1472 - .new-ad-plus { font-size: 20px; line-height: 1; } 1473 - .new-ad-label { font-size: var(--text-xs); font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; } 1474 - .ads-status-filters { display: flex; flex-wrap: wrap; gap: 4px; } 1475 - .status-filter { padding: 4px 8px; font-size: var(--text-xs); } 1476 - .status-filter.active { border-color: var(--accent); color: var(--accent); } 1477 - .ad-cards-group:last-child { margin-bottom: 0; } 1478 - .ad-cards-group-label { font-size: var(--text-xs); color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 2px; } 1479 - .ad-card-wrap { position: relative; min-width: 0; height: 100%; } 1480 - .ad-card-wrap .ad-card { position: relative; min-width: 0; min-height: 80px; width: 100%; height: 100%; } 1481 - .ad-card-action { 1482 - position: absolute; top: 8px; right: 8px; 1483 - padding: 8px; display: flex; align-items: center; justify-content: center; 1484 - z-index: 1; flex-shrink: 0; 1485 - } 1486 - .ad-card-action-icon-wrap { 1487 - position: relative; display: inline-flex; width: 18px; height: 18px; 1488 - align-items: center; justify-content: center; flex-shrink: 0; 1489 - } 1490 - .ad-card-action-icon { 1491 - font-size: 16px; line-height: 1; transition: opacity 0.15s; 1492 - display: flex; align-items: center; justify-content: center; 1493 - width: 18px; height: 18px; 1494 - } 1495 - .ad-card-action-icon-action { position: absolute; opacity: 0; } 1496 - .ad-card-action:hover .ad-card-action-icon-state { opacity: 0; } 1497 - .ad-card-action:hover .ad-card-action-icon-action { opacity: 1; } 1498 - .ad-card-action-live { color: var(--green); background: rgba(34,197,94,0.15); border-color: rgba(34,197,94,0.4); } 1499 - .ad-card-action-paused { color: var(--yellow); background: rgba(234,179,8,0.15); border-color: rgba(234,179,8,0.4); } 1500 - .ad-card-action-live:hover, 1501 - .ad-card-action-live:focus-visible { 1502 - background: rgba(234,179,8,0.2) !important; border-color: var(--yellow) !important; color: var(--yellow) !important; 1503 - } 1504 - .ad-card-action-paused:hover, 1505 - .ad-card-action-paused:focus-visible { 1506 - background: rgba(34,197,94,0.2) !important; border-color: var(--green) !important; color: var(--green) !important; 1507 - } 1508 - .ad-card { 1509 - display: flex; flex-direction: column; align-items: flex-start; gap: 4px; 1510 - padding: clamp(10px, 2.5vw, 14px) clamp(12px, 3vw, 16px); 1511 - padding-right: 56px; border: 1px solid var(--border); background: var(--surface); 1512 - color: var(--text); font: inherit; text-align: left; cursor: pointer; 1513 - transition: border-color 0.15s, background 0.15s; box-sizing: border-box; 1514 - min-height: clamp(72px, 18vw, 88px); 1515 - } 1516 - .ad-card:hover { border-color: var(--text); } 1517 - .ad-card.selected { border-color: var(--accent); background: rgba(20,184,166,0.08); } 1518 - .ad-card-head { font-weight: 500; font-size: var(--text-sm); line-height: 1.4; word-break: break-word; } 1519 - .ad-card-dates { font-size: var(--text-xs); color: var(--muted); line-height: 1.4; } 1520 - .ad-card-stats { font-size: var(--text-xs); color: var(--green); display: block; margin-top: 2px; line-height: 1.4; } 1521 - .ad-card-tier { font-size: var(--text-xs); color: var(--muted); text-transform: uppercase; line-height: 1.4; } 1522 - .ad-kpi-box { margin-bottom: 16px; padding: 10px 12px; background: var(--bg); border: 1px solid var(--border); } 1523 - .ad-kpi-box .group-label { margin-bottom: 8px; } 1524 - .kpi-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 16px; } 1525 - .kpi-item { display: flex; flex-direction: column; gap: 2px; } 1526 - .kpi-label { font-size: var(--text-xs); color: var(--muted); text-transform: uppercase; } 1527 - .kpi-value { font-size: var(--text-sm); font-weight: 500; color: var(--green); } 1528 - .chip { font-size: var(--text-xs); font-weight: 500; padding: 2px 6px; border: 1px solid transparent; line-height: 1.5; } 1529 - .chip-live { color: var(--green); background: rgba(34,197,94,0.15); border-color: rgba(34,197,94,0.3); } 1530 - .chip-scheduled { color: var(--accent); background: rgba(20,184,166,0.15); border-color: rgba(20,184,166,0.3); } 1531 - .chip-ended { color: var(--muted); background: rgba(115,115,115,0.1); opacity: 0.9; } 1532 - .chip-paused { color: var(--yellow); background: rgba(234,179,8,0.15); border-color: rgba(234,179,8,0.3); } 1533 - 1534 - #adCards.loading, #calendarWrap.loading { opacity: 0.6; pointer-events: none; } 1535 - .calendar-empty { margin-top: 16px; padding: 24px; border: 1px dashed var(--border); background: var(--surface); text-align: center; max-width: 240px; } 1536 - .calendar-empty-text { color: var(--muted); font-size: var(--text-sm); margin: 0; } 1537 - .calendar-wrap { margin-top: 16px; border: 1px solid var(--border); background: var(--surface); padding: 18px 22px; width: 100%; min-width: 340px; max-width: 100%; } 1538 - .calendar-header { display: flex; align-items: center; justify-content: center; gap: 20px; margin-bottom: 14px; } 1539 - .cal-month-label { font-size: var(--text-md); font-weight: 500; color: var(--text); margin: 0; } 1540 - .cal-nav { padding: 4px 10px; font-size: var(--text-xs); background: transparent; border: 1px solid var(--border); color: var(--muted); cursor: pointer; } 1541 - .cal-nav.cal-today { margin-left: 8px; } 1542 - .cal-nav:hover { color: var(--text); border-color: var(--text); } 1543 - .calendar-grid { display: flex; flex-direction: column; gap: 0; } 1544 - .cal-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); font-size: var(--text-xs); color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; padding-bottom: 8px; border-bottom: 1px solid var(--border); } 1545 - .cal-weekdays span { text-align: center; } 1546 - .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: var(--border); width: 100%; } 1547 - .cal-cell { min-height: 80px; height: auto; padding: 10px; background: var(--surface); display: flex; flex-direction: column; font-size: var(--text-xs); } 1548 - .cal-cell.other-month { background: var(--bg); } 1549 - .cal-cell.other-month .cal-cell-num { color: var(--muted); opacity: 0.6; } 1550 - .cal-cell.today { outline: 1px solid var(--accent); outline-offset: -1px; z-index: 1; } 1551 - .cal-cell.today .cal-cell-num { color: var(--accent); font-weight: 500; } 1552 - .cal-cell.has-conflict { outline: 1px dashed var(--yellow); outline-offset: -1px; } 1553 - .cal-cell-num { font-size: var(--text-sm); color: var(--text); margin-bottom: 4px; } 1554 - .cal-cell-ads { display: flex; flex-wrap: wrap; gap: 2px; align-content: flex-start; overflow: hidden; } 1555 - .cal-ad-pill { font-size: var(--text-xs); padding: 5px 8px; min-height: 28px; border: none; cursor: pointer; font-family: var(--font); text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } 1556 - .cal-ad-pill.chip-live { background: rgba(34,197,94,0.2); color: var(--green); } 1557 - .cal-ad-pill.chip-scheduled { background: rgba(20,184,166,0.2); color: var(--accent); } 1558 - .cal-ad-pill.chip-ended { background: rgba(102,102,102,0.2); color: var(--muted); } 1559 - .cal-ad-pill.chip-paused { background: rgba(251,191,36,0.2); color: var(--yellow); } 1560 - .cal-ad-pill:hover { opacity: 0.9; } 1561 - .cal-ad-pill.selected { outline: 1px solid var(--text); outline-offset: 1px; } 1562 - .cal-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); font-size: var(--text-xs); color: var(--muted); } 1563 - .cal-legend-dot { display: inline-block; width: 6px; height: 6px; margin-right: 4px; vertical-align: middle; } 1564 - .cal-legend-dot.live { background: var(--green); } 1565 - .cal-legend-dot.scheduled { background: var(--accent); } 1566 - .cal-legend-dot.ended { background: var(--muted); } 1567 - .cal-legend-dot.paused { background: var(--yellow); } 1568 - 1569 - .admin-editor-grid { display: grid; grid-template-columns: 1fr minmax(280px, 393px); gap: 20px; min-width: 0; } 1570 - .preview-pane { position: sticky; top: 0; align-self: start; padding-top: 0; } 1571 - .preview-pane > :first-child { margin-top: 0; } 1572 - .id-version-row { display: flex; flex-direction: row; gap: 8px; } 1573 - .id-version-row input:first-child { flex: 1; min-width: 0; } 1574 - .id-version-row .version-input { width: 64px; min-width: 64px; flex-shrink: 0; } 1575 - .id-version-header { min-width: 0; max-width: 320px; justify-self: center; } 1576 - .id-version-header input { min-height: 36px; padding: 6px 10px; font-size: var(--text-sm); } 1577 - .ad-preview { border: 1px solid var(--border); padding: 0; background: var(--surface); min-height: 8rem; overflow: hidden; } 1578 - .preview { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; font-size: var(--text-base); text-align: left; width: 100%; max-width: 100%; } 1579 - .preview.preview-text { padding: 16px; } 1580 - .preview.preview-banner .preview-copy-block, 1581 - .preview.preview-feature .preview-copy-block { padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; width: 100%; } 1582 - .preview-sponsor-line { display: flex; align-items: center; gap: 8px; } 1583 - .preview-sponsor { font-size: var(--text-sm); color: var(--muted); } 1584 - .preview-logo { width: 24px; height: 24px; object-fit: contain; flex-shrink: 0; } 1585 - .preview-copy { display: flex; flex-direction: column; gap: 10px; width: 100%; } 1586 - .preview-headline { font-size: var(--text-xl); font-weight: 600; line-height: 1.3; } 1587 - .preview-subline { font-size: var(--text-base); color: var(--muted); line-height: 1.4; } 1588 - .preview-cta-wrap { 1589 - display: flex; align-items: center; justify-content: center; gap: 6px; 1590 - width: 100%; padding: 10px 0; 1591 - color: var(--cta-orange); background: rgba(232, 119, 34, 0.12); 1592 - font-size: var(--text-base); font-weight: 500; cursor: default; border: none; 1593 - } 1594 - .preview-cta-arrow { font-size: var(--text-sm); opacity: 0.9; } 1595 - .preview-img-wrap { width: 100%; overflow: hidden; position: relative; background: var(--border); } 1596 - .preview-img { width: 100%; height: 100%; object-fit: cover; display: block; } 1597 - .preview-img-wrap.preview-img-error { background: var(--border); } 1598 - .preview-img-wrap.preview-img-error .preview-img { display: none; } 1599 - .preview-img-placeholder { width: 100%; background: var(--border); display: flex; align-items: center; justify-content: center; font-size: var(--text-sm); color: var(--muted); } 1600 - 1601 - ::-webkit-scrollbar { width: 4px; height: 4px; } 1602 - ::-webkit-scrollbar-thumb { background: var(--border); } 1603 - 1604 - .flatpickr-calendar { z-index: 99999; } 1605 - .flatpickr-day.selected, .flatpickr-day.startRange, .flatpickr-day.endRange { background: var(--accent) !important; border-color: var(--accent) !important; } 1606 - .flatpickr-day:hover { background: var(--border) !important; border-color: var(--border) !important; } 1607 - #start_at[readonly], #end_at[readonly] { cursor: pointer; } 1608 - 1609 - @media (min-width: 900px) { 1610 - .schedule-grid { grid-template-columns: 1fr minmax(420px, 560px); } 1611 - } 1612 - @media (min-width: 1200px) { 1613 - .schedule-grid { grid-template-columns: 1fr minmax(480px, 640px); } 1614 - .cal-cell { min-height: 88px; } 1615 - .cal-ad-pill { font-size: var(--text-xs); min-height: 30px; } 1616 - } 1617 - 1618 - @media (max-width: 768px) { 1619 - .form-overlay { padding: 0; align-items: stretch; justify-content: stretch; } 1620 - .form-overlay-backdrop { display: none; } 1621 - .form-overlay-panel { 1622 - width: 100%; height: 100%; max-width: none; max-height: none; 1623 - border: none; border-radius: 0; 1624 - } 1625 - .admin-editor-grid { grid-template-columns: 1fr; } 1626 - .preview-pane { position: static; } 1627 - .calendar-wrap { max-width: 100%; padding: 10px 12px; min-width: 0; } 1628 - .cal-cell { min-height: 56px; padding: 6px; } 1629 - .cal-cell-num { font-size: var(--text-xs); } 1630 - .cal-ad-pill { font-size: var(--text-xs); padding: 4px 6px; min-height: 36px; } 1631 - .topbar-refresh { 1632 - min-width: 44px; min-height: 44px; padding: 0 12px; 1633 - display: flex; align-items: center; justify-content: center; 1634 - border: none; border-left: 1px solid var(--border); border-right: 1px solid var(--border); 1635 - border-radius: 0; 1636 - } 1637 - .topbar-refresh:hover { border-left-color: var(--accent); border-right-color: var(--accent); } 1638 - .status-filter { min-height: 44px; min-width: 44px; padding: 10px 12px; display: inline-flex; align-items: center; justify-content: center; } 1639 - .cal-nav { min-height: 44px; min-width: 44px; padding: 10px; display: inline-flex; align-items: center; justify-content: center; } 1640 - .cal-nav.cal-today { min-width: auto; padding: 10px 14px; } 1641 - .form-overlay-header { grid-template-columns: 1fr auto; grid-template-rows: auto auto; gap: 12px; } 1642 - .form-overlay-header h2 { grid-column: 1; grid-row: 1; } 1643 - .form-overlay-header > button[aria-label="Close"] { grid-column: 2; grid-row: 1; min-width: 44px; min-height: 44px; padding: 10px; } 1644 - .active-inactive-toggle.active-inactive-header { grid-column: 1 / -1; grid-row: 2; justify-self: stretch; margin-top: 0; } 1645 - .form-overlay-header .active-inactive-btn { min-height: 44px; } 1646 - } 1647 - @media (max-width: 640px) { 1648 - :root { 1649 - --text-xs: 12px; 1650 - --text-sm: 13px; 1651 - --text-base: 15px; 1652 - --text-md: 16px; 1653 - } 1654 - .dashboard-wrap { grid-template-rows: 40px 1fr; } 1655 - .main { padding: 12px 16px; } 1656 - .overview-status-banner { padding: 10px 12px; margin-bottom: 12px; } 1657 - .status-banner-text { font-size: var(--text-sm); } 1658 - .status-banner-sub { font-size: var(--text-xs); } 1659 - .calendar-wrap { margin-top: 12px; min-width: 0; } 1660 - .cal-cell { min-height: 50px; padding: 5px; font-size: var(--text-xs); } 1661 - .cal-legend { margin-top: 10px; padding-top: 10px; gap: 8px; font-size: var(--text-xs); } 1662 - .ad-card { padding: 10px 12px; min-width: 0; padding-right: 52px; } 1663 - .ad-card-action { min-width: 44px; min-height: 44px; padding: 10px; } 1664 - .ad-card-action-icon-wrap { width: 20px; height: 20px; } 1665 - .ad-card-action-icon { font-size: 18px; width: 20px; height: 20px; } 1666 - .form-actions, .form-actions-right { gap: 8px; } 1667 - .date-presets button { min-height: 44px; min-width: 44px; padding: 10px 14px; } 1668 - .tier-btn, .active-inactive-btn { min-height: 44px; } 1669 - .btn-as-link { min-height: 44px; padding: 10px 16px; } 1670 - } 1671 - @media (max-width: 400px) { 1672 - .topbar { padding: 0 8px; gap: 8px; } 1673 - .topbar-sub { display: none; } 1674 - .topbar-sep { display: none; } 1675 - .topbar-title { font-size: var(--text-sm); } 1676 - .topbar-home { font-size: var(--text-sm); } 1677 - .main-toolbar { flex-direction: column; align-items: stretch; } 1678 - .toolbar-search { max-width: none; } 1679 - .form-actions { flex-direction: column; align-items: stretch; } 1680 - .form-actions-right { margin-left: 0; flex-wrap: wrap; } 1681 - .form-actions-right button { flex: 1; min-height: 44px; } 1682 - .delete-btn { min-height: 44px; } 1683 - .new-ad-card { min-height: 88px; } 1684 - .ad-cards { grid-template-columns: 1fr; } 1685 - } 1686 - </style> 1687 1695 </body> 1688 1696 </html>`; 1689 1697
+1570
workers/gymtracker-ads-api/src/ads-landing-html.ts
··· 1 + /** Ads landing page — served at /ads. Public sales page with metrics and mockup wizard. */ 2 + export const ADS_LANDING_HTML = `<!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="utf-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> 7 + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> 8 + <meta http-equiv="Pragma" content="no-cache"> 9 + <title>Advertise on Gym Tracker | A small app. A specific audience. One ad slot.</title> 10 + <meta name="description" content="A few hundred Virginia Tech students use Gym Tracker to check McComas and War Memorial before they go. One ad slot in the main feed. If VT students are who you're trying to reach, it's a direct line."> 11 + <style> 12 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 13 + :root { 14 + --maroon: #861F41; 15 + --maroon-dark: #5c1530; 16 + --maroon-deep: #3a0d1e; 17 + --orange: #E87722; 18 + --orange-dark: #c4611a; 19 + --bg: #f8f5f2; 20 + --surface: #ffffff; 21 + --border: #e8e3de; 22 + --border-strong: #d0c9c3; 23 + --text: #1a1614; 24 + --text-mid: #4a4240; 25 + --muted: #7a7270; 26 + --font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 27 + --font-display: var(--font); 28 + --font-body: var(--font); 29 + --bp-narrow: 480px; 30 + --bp-mobile: 640px; 31 + --bp-tablet: 900px; 32 + } 33 + html { scroll-behavior: smooth; } 34 + body { 35 + font-family: var(--font-body); 36 + font-size: 16px; 37 + line-height: 1.65; 38 + color: var(--text); 39 + background: var(--bg); 40 + -webkit-font-smoothing: antialiased; 41 + overflow-x: hidden; 42 + } 43 + a { color: var(--maroon); text-decoration: none; } 44 + a:hover { text-decoration: underline; } 45 + .container { max-width: 1080px; margin: 0 auto; padding: 0 clamp(16px, 5vw, 24px); } 46 + 47 + /* ── Nav ──────────────────────────────── */ 48 + .nav { 49 + position: sticky; top: 0; z-index: 100; 50 + background: var(--maroon-deep); 51 + border-bottom: 1px solid rgba(255,255,255,0.08); 52 + } 53 + .nav-inner { 54 + display: flex; 55 + align-items: center; 56 + justify-content: space-between; 57 + flex-wrap: wrap; 58 + padding: 14px clamp(16px, 5vw, 24px); 59 + } 60 + .nav-logo { 61 + display: flex; 62 + align-items: center; 63 + gap: 10px; 64 + font-family: var(--font-display); 65 + font-weight: 700; 66 + font-size: 1.25rem; 67 + letter-spacing: 0.08em; 68 + color: #fff; 69 + } 70 + .nav-logo:hover { text-decoration: none; opacity: 0.85; } 71 + .nav-logo-img { 72 + width: 40px; 73 + height: 40px; 74 + object-fit: contain; 75 + } 76 + .nav-links { display: flex; gap: 28px; } 77 + .nav-links a { font-size: 0.875rem; font-weight: 500; color: rgba(255,255,255,0.6); transition: color 0.15s; } 78 + .nav-links a:hover { color: #fff; text-decoration: none; } 79 + .nav-toggle { 80 + display: none; 81 + align-items: center; 82 + justify-content: center; 83 + width: 44px; 84 + height: 44px; 85 + padding: 0; 86 + border: none; 87 + background: transparent; 88 + color: rgba(255,255,255,0.9); 89 + cursor: pointer; 90 + border-radius: 0; 91 + -webkit-tap-highlight-color: transparent; 92 + } 93 + .nav-toggle:hover { background: rgba(255,255,255,0.1); color: #fff; } 94 + .nav-toggle-icon { 95 + display: flex; 96 + flex-direction: column; 97 + gap: 6px; 98 + width: 20px; 99 + } 100 + .nav-toggle-icon span { 101 + display: block; 102 + height: 2px; 103 + background: currentColor; 104 + border-radius: 0; 105 + transition: transform 0.2s, opacity 0.2s; 106 + } 107 + .nav-toggle[aria-expanded="true"] .nav-toggle-icon span:nth-child(1) { 108 + transform: translateY(8px) rotate(45deg); 109 + } 110 + .nav-toggle[aria-expanded="true"] .nav-toggle-icon span:nth-child(2) { 111 + opacity: 0; 112 + } 113 + .nav-toggle[aria-expanded="true"] .nav-toggle-icon span:nth-child(3) { 114 + transform: translateY(-8px) rotate(-45deg); 115 + } 116 + @media (max-width: 640px) { 117 + .nav-toggle { display: flex; } 118 + .nav-links { 119 + display: none; 120 + flex-direction: column; 121 + width: 100%; 122 + gap: 0; 123 + padding-top: 8px; 124 + border-top: 1px solid rgba(255,255,255,0.1); 125 + margin-top: 8px; 126 + order: 10; 127 + } 128 + .nav-links a { 129 + padding: 14px 0; 130 + border-bottom: 1px solid rgba(255,255,255,0.06); 131 + font-size: 1rem; 132 + } 133 + .nav-links a:last-child { border-bottom: none; } 134 + .nav.nav-open .nav-links { display: flex; } 135 + } 136 + 137 + /* ── Hero ─────────────────────────────── */ 138 + .hero { 139 + background: var(--maroon-deep); 140 + padding: clamp(40px, 8vw, 72px) 0 0; 141 + overflow: hidden; 142 + position: relative; 143 + } 144 + .hero::before { 145 + content: ''; 146 + position: absolute; 147 + top: -80px; right: -120px; 148 + width: 480px; height: 480px; 149 + background: radial-gradient(circle, rgba(134,31,65,0.5) 0%, transparent 70%); 150 + pointer-events: none; 151 + } 152 + .hero-content { position: relative; z-index: 1; } 153 + .hero-eyebrow { 154 + display: inline-block; 155 + font-size: 0.75rem; 156 + font-weight: 700; 157 + letter-spacing: 0.14em; 158 + text-transform: uppercase; 159 + color: var(--orange); 160 + margin-bottom: 16px; 161 + } 162 + .hero h1 { 163 + font-family: var(--font-display); 164 + font-weight: 700; 165 + font-size: clamp(3rem, 8vw, 6rem); 166 + line-height: 0.95; 167 + letter-spacing: 0.02em; 168 + color: #fff; 169 + max-width: 680px; 170 + margin-bottom: 24px; 171 + overflow-wrap: break-word; 172 + } 173 + .hero h1 em { color: var(--orange); font-style: normal; } 174 + .hero-sub { 175 + font-size: 1.0625rem; 176 + color: rgba(255,255,255,0.65); 177 + max-width: 480px; 178 + margin-bottom: 36px; 179 + line-height: 1.7; 180 + } 181 + .hero-actions { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; } 182 + .btn-primary { 183 + display: inline-flex; align-items: center; gap: 8px; 184 + padding: 13px 26px; 185 + background: var(--orange); 186 + color: #fff; 187 + font-weight: 700; 188 + font-size: 0.9375rem; 189 + border-radius: 0; 190 + border: none; cursor: pointer; 191 + text-decoration: none; 192 + transition: background 0.15s, transform 0.1s; 193 + } 194 + .btn-primary:hover { background: var(--orange-dark); text-decoration: none; transform: translateY(-1px); } 195 + .btn-ghost { 196 + display: inline-flex; align-items: center; gap: 6px; 197 + padding: 13px 22px; 198 + color: rgba(255,255,255,0.7); 199 + font-weight: 500; 200 + font-size: 0.9375rem; 201 + border-radius: 0; 202 + border: 1px solid rgba(255,255,255,0.2); 203 + text-decoration: none; 204 + transition: all 0.15s; 205 + } 206 + .btn-ghost:hover { color: #fff; border-color: rgba(255,255,255,0.5); text-decoration: none; } 207 + .hero-stats { 208 + display: grid; 209 + grid-template-columns: repeat(3, 1fr); 210 + border-top: 1px solid rgba(255,255,255,0.1); 211 + margin-top: 56px; 212 + } 213 + @media (max-width: 640px) { .hero-stats { grid-template-columns: repeat(2, 1fr); } } 214 + .hero-stat { 215 + padding: clamp(16px, 4vw, 28px) clamp(16px, 4vw, 20px); 216 + border-right: 1px solid rgba(255,255,255,0.1); 217 + display: flex; 218 + flex-direction: column; 219 + align-items: center; 220 + justify-content: center; 221 + text-align: center; 222 + } 223 + .hero-stat:nth-child(2n) { border-right: none; } 224 + @media (min-width: 641px) { 225 + .hero-stat:nth-child(2) { border-right: 1px solid rgba(255,255,255,0.1); } 226 + .hero-stat:nth-child(3) { border-right: none; } 227 + } 228 + .hero-stat-val { 229 + font-family: var(--font-display); 230 + font-weight: 700; 231 + font-size: 2.5rem; 232 + letter-spacing: 0.02em; 233 + color: #fff; 234 + line-height: 1; 235 + margin-bottom: 6px; 236 + } 237 + .hero-stat-label { 238 + font-size: 0.8125rem; 239 + color: rgba(255,255,255,0.5); 240 + font-weight: 500; 241 + line-height: 1.3; 242 + max-width: 10em; 243 + overflow-wrap: break-word; 244 + } 245 + .hero-stat-note { 246 + grid-column: 1 / -1; 247 + font-size: 0.75rem; 248 + color: rgba(255,255,255,0.4); 249 + margin: 0; 250 + padding: 16px 20px 0; 251 + border-top: 1px solid rgba(255,255,255,0.06); 252 + text-align: center; 253 + line-height: 1.5; 254 + } 255 + 256 + /* ── Sections ─────────────────────────── */ 257 + .section { padding: clamp(48px, 10vw, 72px) 0; } 258 + .section-alt { background: var(--surface); } 259 + .section-dark { background: var(--maroon-deep); } 260 + .section-label { 261 + font-size: 0.75rem; 262 + font-weight: 700; 263 + letter-spacing: 0.14em; 264 + text-transform: uppercase; 265 + color: var(--orange); 266 + margin-bottom: 10px; 267 + } 268 + .section-title { 269 + font-family: var(--font-display); 270 + font-weight: 700; 271 + font-size: 2.75rem; 272 + letter-spacing: 0.03em; 273 + color: var(--text); 274 + line-height: 1; 275 + margin-bottom: 12px; 276 + overflow-wrap: break-word; 277 + } 278 + .section-title.on-dark { color: #fff; } 279 + .section-sub { 280 + font-size: 1rem; 281 + color: var(--muted); 282 + max-width: 520px; 283 + line-height: 1.7; 284 + } 285 + .section-header { margin-bottom: clamp(32px, 6vw, 44px); } 286 + 287 + /* ── Formats ──────────────────────────── */ 288 + .formats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } 289 + @media (max-width: 640px) { 290 + .formats-grid { grid-template-columns: 1fr; gap: 16px; } 291 + .format-card-preview { min-height: 100px; padding: 16px; } 292 + } 293 + .format-card { 294 + background: var(--bg); 295 + border: 1px solid var(--border); 296 + border-radius: 0; 297 + overflow: hidden; 298 + } 299 + .format-card-preview { 300 + padding: 20px; 301 + background: var(--surface); 302 + border-bottom: 1px solid var(--border); 303 + min-height: 120px; 304 + display: flex; 305 + align-items: stretch; 306 + } 307 + .format-preview-text { 308 + width: 100%; 309 + display: flex; 310 + flex-direction: column; 311 + justify-content: center; 312 + gap: 6px; 313 + padding: 12px; 314 + border-radius: 0; 315 + border: 1px solid var(--border); 316 + background: var(--bg); 317 + } 318 + .format-preview-banner { 319 + width: 100%; 320 + display: flex; 321 + flex-direction: column; 322 + gap: 0; 323 + border-radius: 0; 324 + border: 1px solid var(--border); 325 + overflow: hidden; 326 + background: var(--bg); 327 + } 328 + .format-preview-img { 329 + height: 70px; 330 + background: linear-gradient(135deg, var(--border) 0%, var(--border-strong) 100%); 331 + display: flex; 332 + align-items: center; 333 + justify-content: center; 334 + } 335 + .format-preview-img span { font-size: 11px; color: var(--muted); font-weight: 500; } 336 + .format-preview-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 4px; } 337 + .fph { width: 60%; height: 8px; background: var(--border-strong); border-radius: 0; } 338 + .fps { width: 45%; height: 6px; background: var(--border); border-radius: 0; } 339 + .fpc { width: 32%; height: 6px; background: var(--orange); border-radius: 0; opacity: 0.6; margin-top: 4px; } 340 + .format-preview-feature .format-preview-img { height: 110px; } 341 + .format-card-info { padding: 18px 20px; } 342 + .format-card-name { 343 + font-family: var(--font-display); 344 + font-weight: 700; 345 + font-size: 1.5rem; 346 + letter-spacing: 0.04em; 347 + color: var(--maroon); 348 + margin-bottom: 4px; 349 + } 350 + .format-card-desc { font-size: 0.875rem; color: var(--muted); line-height: 1.6; } 351 + 352 + /* ── Exclusive callout ─────────────────────────────────── */ 353 + .exclusive-callout { 354 + display: grid; 355 + grid-template-columns: 1fr auto; 356 + gap: 24px; 357 + align-items: center; 358 + padding: 32px; 359 + background: var(--maroon-deep); 360 + border-radius: 0; 361 + margin-bottom: 16px; 362 + } 363 + @media (max-width: 640px) { 364 + .exclusive-callout { 365 + grid-template-columns: 1fr; 366 + padding: 24px; 367 + gap: 20px; 368 + } 369 + } 370 + @media (max-width: 480px) { 371 + .exclusive-callout { padding: 20px; gap: 16px; } 372 + } 373 + .exclusive-tag { 374 + display: inline-block; 375 + font-size: 0.6875rem; 376 + font-weight: 700; 377 + letter-spacing: 0.12em; 378 + text-transform: uppercase; 379 + color: var(--orange); 380 + margin-bottom: 12px; 381 + } 382 + .exclusive-headline { 383 + font-family: var(--font-display); 384 + font-weight: 700; 385 + font-size: 2.25rem; 386 + letter-spacing: 0.03em; 387 + color: #fff; 388 + line-height: 1; 389 + margin-bottom: 12px; 390 + overflow-wrap: break-word; 391 + } 392 + .exclusive-desc { 393 + font-size: 0.9375rem; 394 + color: rgba(255,255,255,0.6); 395 + line-height: 1.65; 396 + max-width: 420px; 397 + } 398 + .exclusive-right { flex-shrink: 0; } 399 + .exclusive-stat-row { 400 + display: flex; 401 + align-items: stretch; 402 + gap: 0; 403 + border: 1px solid rgba(255,255,255,0.12); 404 + border-radius: 0; 405 + overflow: hidden; 406 + } 407 + @media (max-width: 480px) { 408 + .exclusive-stat-row { flex-direction: column; } 409 + .exclusive-stat-divider { width: 100%; height: 1px; background: rgba(255,255,255,0.1); } 410 + } 411 + .exclusive-stat { 412 + display: flex; 413 + flex-direction: column; 414 + align-items: center; 415 + justify-content: center; 416 + gap: 4px; 417 + padding: 20px 28px; 418 + background: rgba(255,255,255,0.04); 419 + } 420 + @media (max-width: 480px) { 421 + .exclusive-stat { padding: 16px 20px; } 422 + .exclusive-stat-num { font-size: 2rem; } 423 + } 424 + .exclusive-stat-divider { 425 + width: 1px; 426 + background: rgba(255,255,255,0.1); 427 + } 428 + .exclusive-stat-num { 429 + font-family: var(--font-display); 430 + font-weight: 700; 431 + font-size: 2.5rem; 432 + color: #fff; 433 + letter-spacing: 0.04em; 434 + line-height: 1; 435 + } 436 + .exclusive-stat-label { 437 + font-size: 0.75rem; 438 + font-weight: 600; 439 + color: rgba(255,255,255,0.45); 440 + text-align: center; 441 + white-space: nowrap; 442 + } 443 + 444 + /* ── Feature cards ─────────────────────────────────────── */ 445 + .features-grid { 446 + display: grid; 447 + grid-template-columns: repeat(2, 1fr); 448 + gap: 16px; 449 + margin-bottom: 16px; 450 + } 451 + @media (max-width: 640px) { .features-grid { grid-template-columns: 1fr; } } 452 + .feature-card { 453 + padding: 24px; 454 + background: var(--bg); 455 + border: 1px solid var(--border); 456 + border-radius: 0; 457 + display: flex; 458 + flex-direction: column; 459 + gap: 12px; 460 + } 461 + .feature-card-tag { 462 + display: inline-block; 463 + align-self: flex-start; 464 + font-size: 0.6875rem; 465 + font-weight: 600; 466 + letter-spacing: 0.12em; 467 + text-transform: uppercase; 468 + color: var(--text-mid); 469 + background: rgba(74,66,64,0.08); 470 + padding: 4px 10px; 471 + border-radius: 0; 472 + } 473 + .feature-title { 474 + font-weight: 700; 475 + font-size: 1rem; 476 + color: var(--text); 477 + line-height: 1.35; 478 + overflow-wrap: break-word; 479 + } 480 + .feature-desc { 481 + font-size: 0.875rem; 482 + font-weight: 400; 483 + color: var(--muted); 484 + line-height: 1.65; 485 + } 486 + 487 + /* ── Peaks timeline ────────────────────────────────────── */ 488 + .peaks-card { 489 + padding: 24px; 490 + background: var(--bg); 491 + border: 1px solid var(--border); 492 + border-radius: 0; 493 + margin-bottom: 16px; 494 + } 495 + @media (max-width: 480px) { 496 + .peaks-card { padding: 16px; } 497 + } 498 + .peaks-header { 499 + display: flex; 500 + align-items: baseline; 501 + justify-content: space-between; 502 + flex-wrap: wrap; 503 + margin-bottom: 20px; 504 + gap: 12px; 505 + } 506 + .peaks-title { 507 + font-weight: 700; 508 + font-size: 0.9375rem; 509 + color: var(--text); 510 + } 511 + .peaks-note { 512 + font-size: 0.8rem; 513 + color: var(--muted); 514 + } 515 + .peaks-track-wrap { 516 + overflow-x: auto; 517 + -webkit-overflow-scrolling: touch; 518 + margin-bottom: 8px; 519 + } 520 + .peaks-track { 521 + display: grid; 522 + grid-template-columns: repeat(12, 1fr); 523 + gap: 4px; 524 + min-width: 320px; 525 + } 526 + .peaks-bar-wrap { 527 + display: flex; 528 + flex-direction: column; 529 + align-items: center; 530 + gap: 4px; 531 + } 532 + .peaks-bar { 533 + width: 100%; 534 + border-radius: 0; 535 + background: var(--border); 536 + transition: background 0.2s; 537 + } 538 + .peaks-bar.peak { background: var(--maroon); } 539 + .peaks-bar.off-peak { background: var(--border-strong); opacity: 0.6; } 540 + .peaks-bar.summer { background: var(--border); opacity: 0.4; } 541 + .peaks-month { 542 + font-size: 0.625rem; 543 + font-weight: 600; 544 + letter-spacing: 0.04em; 545 + text-transform: uppercase; 546 + color: var(--muted); 547 + } 548 + .peaks-legend { 549 + display: flex; 550 + flex-wrap: wrap; 551 + gap: 16px; 552 + margin-top: 12px; 553 + } 554 + .peaks-legend-item { 555 + display: flex; 556 + align-items: center; 557 + gap: 6px; 558 + font-size: 0.75rem; 559 + color: var(--muted); 560 + } 561 + .peaks-legend-dot { 562 + width: 10px; 563 + height: 10px; 564 + border-radius: 0; 565 + } 566 + 567 + /* ── Social proof ──────────────────────────────────────── */ 568 + .social-proof { 569 + margin-bottom: 16px; 570 + } 571 + .social-proof-label { 572 + font-size: 0.75rem; 573 + font-weight: 700; 574 + letter-spacing: 0.12em; 575 + text-transform: uppercase; 576 + color: var(--muted); 577 + margin-bottom: 12px; 578 + } 579 + .social-quotes { 580 + display: grid; 581 + grid-template-columns: repeat(3, 1fr); 582 + gap: 12px; 583 + } 584 + @media (max-width: 640px) { .social-quotes { grid-template-columns: 1fr; } } 585 + .social-quote { 586 + padding: 16px 18px; 587 + background: var(--bg); 588 + border: 1px solid var(--border); 589 + border-radius: 0; 590 + display: flex; 591 + flex-direction: column; 592 + gap: 8px; 593 + } 594 + .social-quote-text { 595 + font-size: 0.9375rem; 596 + color: var(--text); 597 + line-height: 1.5; 598 + font-style: italic; 599 + overflow-wrap: break-word; 600 + } 601 + .social-quote-text::before { content: '\\201C'; color: var(--maroon); font-style: normal; font-weight: 700; margin-right: 1px; } 602 + .social-quote-text::after { content: '\\201D'; color: var(--maroon); font-style: normal; font-weight: 700; margin-left: 1px; } 603 + .social-quote-source { 604 + font-size: 0.75rem; 605 + color: var(--muted); 606 + font-weight: 600; 607 + } 608 + 609 + /* ── Proof strip ───────────────────────────────────────── */ 610 + .proof-strip { 611 + display: grid; 612 + grid-template-columns: repeat(3, 1fr); 613 + gap: 16px; 614 + } 615 + @media (max-width: 640px) { .proof-strip { grid-template-columns: 1fr; } } 616 + .proof-item { 617 + padding: 20px; 618 + border: 1px solid var(--border); 619 + border-radius: 0; 620 + background: var(--bg); 621 + display: flex; 622 + flex-direction: column; 623 + gap: 8px; 624 + } 625 + .proof-icon { 626 + width: 32px; 627 + height: 32px; 628 + border-radius: 0; 629 + background: rgba(134,31,65,0.07); 630 + display: flex; 631 + align-items: center; 632 + justify-content: center; 633 + } 634 + .proof-icon svg { width: 15px; height: 15px; } 635 + .proof-title { 636 + font-size: 0.875rem; 637 + font-weight: 700; 638 + color: var(--text); 639 + } 640 + .proof-desc { 641 + font-size: 0.8125rem; 642 + color: var(--muted); 643 + line-height: 1.55; 644 + } 645 + 646 + /* ── Wizard (matches admin editor grid) ───────────────────────────── */ 647 + .wizard-grid { display: grid; grid-template-columns: 1fr minmax(280px, 393px); gap: 20px; align-items: start; min-width: 0; } 648 + .admin-form { display: flex; flex-direction: column; gap: 0; } 649 + .admin-form .form-group { margin-bottom: 18px; } 650 + @media (max-width: 900px) { 651 + .wizard-grid { grid-template-columns: 1fr; } 652 + .preview-pane { position: static; } 653 + } 654 + .form-group { margin-bottom: 18px; } 655 + .form-label { 656 + display: block; 657 + font-size: 0.8125rem; 658 + font-weight: 600; 659 + color: var(--text-mid); 660 + margin-bottom: 7px; 661 + letter-spacing: 0.01em; 662 + } 663 + .form-input { 664 + width: 100%; 665 + padding: 10px 14px; 666 + border: 1px solid var(--border-strong); 667 + border-radius: 0; 668 + font-size: 0.9375rem; 669 + font-family: var(--font-body); 670 + color: var(--text); 671 + background: var(--surface); 672 + transition: border-color 0.15s, box-shadow 0.15s; 673 + appearance: none; 674 + } 675 + .form-input:focus { 676 + outline: none; 677 + border-color: var(--maroon); 678 + box-shadow: 0 0 0 3px rgba(134,31,65,0.1); 679 + } 680 + .form-input::placeholder { color: #b5afaa; } 681 + .form-hint { font-size: 0.8rem; color: var(--muted); margin-top: 5px; } 682 + .form-file-row { display: flex; flex-direction: column; gap: 8px; } 683 + .form-file-input { 684 + font-size: 0.875rem; font-family: var(--font-body); color: var(--text); 685 + padding: 6px 0; border: none; background: transparent; 686 + } 687 + #imageFieldWrap { 688 + overflow: hidden; 689 + max-height: 160px; 690 + opacity: 1; 691 + transition: opacity 0.2s ease, max-height 0.25s ease; 692 + } 693 + #imageFieldWrap.image-field-hidden { 694 + opacity: 0; 695 + max-height: 0; 696 + margin-bottom: 0; 697 + pointer-events: none; 698 + } 699 + 700 + .tier-toggle { 701 + display: flex; 702 + width: 100%; 703 + min-width: 0; 704 + margin-top: 12px; 705 + border: 1px solid var(--border); 706 + border-radius: 0; 707 + overflow: hidden; 708 + } 709 + .tier-btn { 710 + flex: 1; 711 + padding: 10px 16px; 712 + font-size: 0.875rem; 713 + font-weight: 500; 714 + border: none; 715 + background: transparent; 716 + color: var(--muted); 717 + cursor: pointer; 718 + transition: all 0.15s; 719 + touch-action: manipulation; 720 + -webkit-tap-highlight-color: transparent; 721 + } 722 + .tier-btn:not(:last-child) { border-right: 1px solid var(--border); } 723 + .tier-btn:hover { color: var(--text); } 724 + .tier-btn.active { background: rgba(134,31,65,0.12); color: var(--maroon); font-weight: 600; } 725 + 726 + /* ── Preview Pane (matches admin) ─────────────────────── */ 727 + .preview-pane { position: sticky; top: 84px; min-width: 0; } 728 + .ad-preview { padding: 0; background: transparent; min-height: 8rem; overflow: hidden; } 729 + .preview { 730 + display: flex; 731 + flex-direction: column; 732 + align-items: flex-start; 733 + text-align: left; 734 + width: 100%; 735 + max-width: 100%; 736 + background: var(--surface); 737 + border: 1px solid var(--border); 738 + border-radius: 0; 739 + overflow: hidden; 740 + } 741 + .preview-header { 742 + display: flex; 743 + align-items: center; 744 + gap: 8px; 745 + width: 100%; 746 + padding: 0 16px 8px; 747 + border-bottom: 1px solid var(--border); 748 + margin-bottom: 0; 749 + } 750 + .preview.preview-text { padding: 16px; gap: 12px; } 751 + .preview.preview-text .preview-header { padding: 0 0 8px; margin-bottom: 4px; } 752 + .preview.preview-banner .preview-header, 753 + .preview.preview-feature .preview-header { padding: 12px 16px 8px; } 754 + .preview.preview-banner .preview-copy-block, 755 + .preview.preview-feature .preview-copy-block { 756 + padding: 14px 16px; 757 + display: flex; 758 + flex-direction: column; 759 + gap: 12px; 760 + width: 100%; 761 + } 762 + .preview-header-logo { 763 + width: 20px; 764 + height: 20px; 765 + border-radius: 0; 766 + object-fit: contain; 767 + flex-shrink: 0; 768 + } 769 + .preview-header-sponsor { 770 + font-size: 0.875rem; 771 + font-weight: 500; 772 + color: var(--text); 773 + flex: 1; 774 + } 775 + .preview-header-sponsored { 776 + font-size: 0.75rem; 777 + color: var(--muted); 778 + } 779 + .preview-copy { 780 + display: flex; 781 + flex-direction: column; 782 + gap: 10px; 783 + width: 100%; 784 + } 785 + .preview-headline { 786 + font-size: 1rem; 787 + font-weight: 600; 788 + color: var(--text); 789 + line-height: 1.3; 790 + margin: 0; 791 + } 792 + .preview-subline { 793 + font-size: 0.875rem; 794 + color: var(--muted); 795 + line-height: 1.4; 796 + margin: 0; 797 + } 798 + .preview-img-wrap { 799 + width: 100%; 800 + overflow: hidden; 801 + position: relative; 802 + background: var(--border); 803 + } 804 + .preview-img-wrap.preview-img-error { background: var(--border); } 805 + .preview-img-wrap.preview-img-error .preview-img { display: none; } 806 + .preview-img { 807 + width: 100%; 808 + height: 100%; 809 + object-fit: cover; 810 + display: block; 811 + } 812 + .preview-img-placeholder { 813 + width: 100%; 814 + background: var(--border); 815 + display: flex; 816 + align-items: center; 817 + justify-content: center; 818 + font-size: 0.875rem; 819 + color: var(--muted); 820 + } 821 + .preview-cta-wrap { 822 + display: flex; 823 + align-items: center; 824 + justify-content: center; 825 + gap: 6px; 826 + width: 100%; 827 + padding: 10px; 828 + color: var(--orange); 829 + background: rgba(232, 119, 34, 0.12); 830 + font-size: 0.875rem; 831 + font-weight: 500; 832 + border-radius: 8px; 833 + box-sizing: border-box; 834 + } 835 + .preview-cta-arrow { font-size: 12px; opacity: 0.9; } 836 + .preview-label { 837 + font-size: 0.75rem; 838 + color: var(--muted); 839 + text-align: center; 840 + margin-top: 10px; 841 + font-weight: 500; 842 + } 843 + 844 + .wizard-cta-box { 845 + margin-top: 32px; 846 + padding: 22px; 847 + background: rgba(134,31,65,0.05); 848 + border: 1px solid rgba(134,31,65,0.15); 849 + border-radius: 0; 850 + } 851 + .wizard-cta-box p { font-size: 0.9rem; color: var(--text-mid); margin-bottom: 14px; line-height: 1.65; } 852 + .btn-contact { 853 + display: inline-flex; 854 + align-items: center; 855 + gap: 8px; 856 + padding: 12px 24px; 857 + background: var(--maroon); 858 + color: #fff; 859 + font-weight: 700; 860 + font-size: 0.9375rem; 861 + border-radius: 0; 862 + text-decoration: none; 863 + transition: background 0.15s, transform 0.1s; 864 + } 865 + .btn-contact:hover { background: var(--maroon-dark); text-decoration: none; transform: translateY(-1px); } 866 + 867 + /* ── Contact ──────────────────────────── */ 868 + .contact-inner { max-width: 860px; margin: 0 auto; } 869 + .contact-inner .section-title { color: #fff; text-align: center; } 870 + .contact-inner .section-sub { color: rgba(255,255,255,0.55); margin: 0 auto 28px; text-align: center; max-width: 620px; } 871 + .contact-card { 872 + display: grid; 873 + grid-template-columns: 1.15fr 1fr; 874 + gap: 24px; 875 + align-items: stretch; 876 + padding: 24px; 877 + border: 1px solid rgba(255,255,255,0.12); 878 + border-radius: 0; 879 + background: rgba(255,255,255,0.04); 880 + } 881 + @media (max-width: 640px) { 882 + .contact-card { 883 + grid-template-columns: 1fr; 884 + padding: 20px; 885 + gap: 20px; 886 + } 887 + } 888 + .contact-col-title { 889 + font-family: var(--font-display); 890 + font-weight: 700; 891 + font-size: 1.5rem; 892 + letter-spacing: 0.04em; 893 + color: #fff; 894 + margin-bottom: 8px; 895 + line-height: 1; 896 + } 897 + .contact-col-sub { 898 + font-size: 0.875rem; 899 + color: rgba(255,255,255,0.58); 900 + line-height: 1.65; 901 + margin-bottom: 16px; 902 + } 903 + .contact-points { 904 + list-style: none; 905 + display: grid; 906 + gap: 12px; 907 + margin: 0; 908 + padding: 0; 909 + } 910 + .contact-points li { 911 + font-size: 0.875rem; 912 + color: rgba(255,255,255,0.78); 913 + line-height: 1.55; 914 + padding-left: 16px; 915 + position: relative; 916 + } 917 + .contact-points li::before { 918 + content: ""; 919 + width: 6px; 920 + height: 6px; 921 + border-radius: 0; 922 + background: var(--orange); 923 + position: absolute; 924 + left: 0; 925 + top: 0.55em; 926 + } 927 + .contact-cta-col { 928 + border-left: 4px solid var(--orange); 929 + padding: 24px 24px 24px 28px; 930 + background: rgba(255,255,255,0.03); 931 + display: flex; 932 + flex-direction: column; 933 + justify-content: center; 934 + gap: 16px; 935 + } 936 + .contact-cta-label { 937 + font-size: 0.75rem; 938 + font-weight: 700; 939 + letter-spacing: 0.14em; 940 + text-transform: uppercase; 941 + color: var(--orange); 942 + margin: 0; 943 + } 944 + .btn-email { 945 + display: flex; 946 + align-items: center; 947 + justify-content: center; 948 + width: 100%; 949 + padding: 14px 24px; 950 + background: var(--orange); 951 + color: #fff; 952 + font-weight: 700; 953 + font-size: 1rem; 954 + border-radius: 0; 955 + text-decoration: none; 956 + border: none; 957 + transition: background 0.15s, transform 0.1s; 958 + } 959 + .btn-email:hover { background: var(--orange-dark); text-decoration: none; transform: translateY(-1px); } 960 + .contact-note { margin: 0; font-size: 0.8125rem; color: rgba(255,255,255,0.45); line-height: 1.55; } 961 + 962 + /* ── Footer ───────────────────────────── */ 963 + .footer { 964 + padding: clamp(20px, 4vw, 28px) 0; 965 + border-top: 1px solid rgba(255,255,255,0.08); 966 + background: var(--maroon-deep); 967 + } 968 + .footer-inner { 969 + display: flex; 970 + align-items: center; 971 + justify-content: space-between; 972 + flex-wrap: wrap; 973 + gap: 12px; 974 + } 975 + .footer-copy { font-size: 0.8125rem; color: rgba(255,255,255,0.35); } 976 + .footer-links { display: flex; gap: 20px; } 977 + .footer-links a { font-size: 0.8125rem; color: rgba(255,255,255,0.45); } 978 + .footer-links a:hover { color: rgba(255,255,255,0.8); text-decoration: none; } 979 + </style> 980 + </head> 981 + <body data-ads-version="2024-03-preview-placeholder"> 982 + 983 + <nav class="nav" id="mainNav"> 984 + <div class="container nav-inner"> 985 + <a href="https://gymtracker.jackhannon.net/" class="nav-logo"><img src="/logo.png" alt="" class="nav-logo-img" width="40" height="40">GYM TRACKER</a> 986 + <button type="button" class="nav-toggle" id="navToggle" aria-expanded="false" aria-controls="navLinks" aria-label="Toggle menu"> 987 + <span class="nav-toggle-icon"> 988 + <span></span><span></span><span></span> 989 + </span> 990 + </button> 991 + <div class="nav-links" id="navLinks"> 992 + <a href="#metrics">How it works</a> 993 + <a href="#formats">Formats</a> 994 + <a href="#wizard">Preview</a> 995 + <a href="#contact">Contact</a> 996 + </div> 997 + </div> 998 + </nav> 999 + 1000 + <section class="hero"> 1001 + <div class="container hero-content"> 1002 + <p class="hero-eyebrow">Advertise on Gym Tracker</p> 1003 + <h1>A small app.<br><em>A specific audience.</em><br>One ad slot.</h1> 1004 + <p class="hero-sub">VT students check McComas and War Memorial before they go. Six times a week, on average. One ad slot in the feed. If Hokies are your audience, this is it.</p> 1005 + <div class="hero-actions"> 1006 + <a href="#wizard" class="btn-primary">Build a mockup &rarr;</a> 1007 + <a href="https://apps.apple.com/us/app/vt-gym-tracker/id6736409867?itscg=30200&itsct=apps_box_badge&mttnsubad=6736409867" class="btn-ghost" target="_blank" rel="noopener noreferrer">View on the App Store</a> 1008 + </div> 1009 + <div class="hero-stats"> 1010 + <div class="hero-stat"> 1011 + <div class="hero-stat-val">3K+</div> 1012 + <div class="hero-stat-label">Total installs</div> 1013 + </div> 1014 + <div class="hero-stat"> 1015 + <div class="hero-stat-val">11K+</div> 1016 + <div class="hero-stat-label">Monthly impressions</div> 1017 + </div> 1018 + <div class="hero-stat"> 1019 + <div class="hero-stat-val">360+</div> 1020 + <div class="hero-stat-label">Daily active users</div> 1021 + </div> 1022 + </div> 1023 + </div> 1024 + </section> 1025 + 1026 + <section id="metrics" class="section section-alt"> 1027 + <div class="container"> 1028 + <div class="section-header"> 1029 + <p class="section-label">How it works</p> 1030 + <h2 class="section-title">It's part of their routine</h2> 1031 + <p class="section-sub">Students check occupancy before they go, sometimes more than once a day. The ad is in that same view&mdash;not a separate tab or a notification they opted out of.</p> 1032 + </div> 1033 + 1034 + <div class="exclusive-callout"> 1035 + <div class="exclusive-left"> 1036 + <span class="exclusive-tag">One sponsor at a time</span> 1037 + <p class="exclusive-headline">One ad in the app.<br>Every open. Every user.</p> 1038 + <p class="exclusive-desc">There&rsquo;s no rotation or bidding system. While you&rsquo;re running, every person who opens the app sees your ad.</p> 1039 + </div> 1040 + <div class="exclusive-right"> 1041 + <div class="exclusive-stat-row"> 1042 + <div class="exclusive-stat"> 1043 + <span class="exclusive-stat-num">100%</span> 1044 + <span class="exclusive-stat-label">Share of voice</span> 1045 + </div> 1046 + <div class="exclusive-stat-divider"></div> 1047 + <div class="exclusive-stat"> 1048 + <span class="exclusive-stat-num">0</span> 1049 + <span class="exclusive-stat-label">Competing ads</span> 1050 + </div> 1051 + </div> 1052 + </div> 1053 + </div> 1054 + 1055 + <div class="features-grid"> 1056 + <div class="feature-card"> 1057 + <span class="feature-card-tag">Placement</span> 1058 + <div class="feature-title">Same view as the gym data</div> 1059 + <div class="feature-desc">The ad sits in the feed where users check occupancy. It&rsquo;s not a banner in a corner&mdash;it&rsquo;s in the same place they&rsquo;re already looking.</div> 1060 + </div> 1061 + 1062 + <div class="feature-card"> 1063 + <span class="feature-card-tag">Audience</span> 1064 + <div class="feature-title">That&rsquo;s the whole audience</div> 1065 + <div class="feature-desc">Everyone who has this app goes to Virginia Tech and uses the campus gyms. There&rsquo;s no broader regional audience to filter out.</div> 1066 + </div> 1067 + 1068 + <div class="feature-card"> 1069 + <span class="feature-card-tag">Scheduling</span> 1070 + <div class="feature-title">Pick your dates</div> 1071 + <div class="feature-desc">Set a start and end date. Works for a single event, a semester push, or just a specific week. No long-term commitment.</div> 1072 + </div> 1073 + 1074 + <div class="feature-card"> 1075 + <span class="feature-card-tag">Reporting</span> 1076 + <div class="feature-title">Impressions and clicks, tracked directly</div> 1077 + <div class="feature-desc">No third-party tools or estimates. You&rsquo;ll see exactly how many times the ad was shown and how many people tapped it.</div> 1078 + </div> 1079 + </div> 1080 + 1081 + <div class="peaks-card"> 1082 + <div class="peaks-header"> 1083 + <span class="peaks-title">When students are most active</span> 1084 + <span class="peaks-note">Follows the semester schedule</span> 1085 + </div> 1086 + <div class="peaks-track-wrap"> 1087 + <div class="peaks-track"> 1088 + <div class="peaks-bar-wrap"> 1089 + <div class="peaks-bar peak" style="height:48px"></div> 1090 + <span class="peaks-month">Jan</span> 1091 + </div> 1092 + <div class="peaks-bar-wrap"> 1093 + <div class="peaks-bar peak" style="height:44px"></div> 1094 + <span class="peaks-month">Feb</span> 1095 + </div> 1096 + <div class="peaks-bar-wrap"> 1097 + <div class="peaks-bar peak" style="height:40px"></div> 1098 + <span class="peaks-month">Mar</span> 1099 + </div> 1100 + <div class="peaks-bar-wrap"> 1101 + <div class="peaks-bar peak" style="height:42px"></div> 1102 + <span class="peaks-month">Apr</span> 1103 + </div> 1104 + <div class="peaks-bar-wrap"> 1105 + <div class="peaks-bar off-peak" style="height:24px"></div> 1106 + <span class="peaks-month">May</span> 1107 + </div> 1108 + <div class="peaks-bar-wrap"> 1109 + <div class="peaks-bar summer" style="height:14px"></div> 1110 + <span class="peaks-month">Jun</span> 1111 + </div> 1112 + <div class="peaks-bar-wrap"> 1113 + <div class="peaks-bar summer" style="height:12px"></div> 1114 + <span class="peaks-month">Jul</span> 1115 + </div> 1116 + <div class="peaks-bar-wrap"> 1117 + <div class="peaks-bar peak" style="height:46px"></div> 1118 + <span class="peaks-month">Aug</span> 1119 + </div> 1120 + <div class="peaks-bar-wrap"> 1121 + <div class="peaks-bar peak" style="height:44px"></div> 1122 + <span class="peaks-month">Sep</span> 1123 + </div> 1124 + <div class="peaks-bar-wrap"> 1125 + <div class="peaks-bar peak" style="height:42px"></div> 1126 + <span class="peaks-month">Oct</span> 1127 + </div> 1128 + <div class="peaks-bar-wrap"> 1129 + <div class="peaks-bar peak" style="height:38px"></div> 1130 + <span class="peaks-month">Nov</span> 1131 + </div> 1132 + <div class="peaks-bar-wrap"> 1133 + <div class="peaks-bar off-peak" style="height:18px"></div> 1134 + <span class="peaks-month">Dec</span> 1135 + </div> 1136 + </div> 1137 + </div> 1138 + <div class="peaks-legend"> 1139 + <div class="peaks-legend-item"> 1140 + <div class="peaks-legend-dot" style="background:var(--maroon)"></div> 1141 + <span>Peak semester traffic</span> 1142 + </div> 1143 + <div class="peaks-legend-item"> 1144 + <div class="peaks-legend-dot" style="background:var(--border-strong)"></div> 1145 + <span>Moderate</span> 1146 + </div> 1147 + <div class="peaks-legend-item"> 1148 + <div class="peaks-legend-dot" style="background:var(--border)"></div> 1149 + <span>Summer low</span> 1150 + </div> 1151 + </div> 1152 + </div> 1153 + 1154 + <div class="social-proof"> 1155 + <p class="social-proof-label">A few things students have said</p> 1156 + <div class="social-quotes"> 1157 + <div class="social-quote"> 1158 + <span class="social-quote-text">I be using this all the time ngl. It helped me make an actual good schedule with the gym.</span> 1159 + <span class="social-quote-source">VT student</span> 1160 + </div> 1161 + <div class="social-quote"> 1162 + <span class="social-quote-text">I got this first semester it's so good!</span> 1163 + <span class="social-quote-source">VT student</span> 1164 + </div> 1165 + <div class="social-quote"> 1166 + <span class="social-quote-text">use this app daily bro keep up the good work</span> 1167 + <span class="social-quote-source">VT student</span> 1168 + </div> 1169 + <div class="social-quote"> 1170 + <span class="social-quote-text">wait this is actually so cool, just downloaded</span> 1171 + <span class="social-quote-source">VT student</span> 1172 + </div> 1173 + <div class="social-quote"> 1174 + <span class="social-quote-text">Peak app</span> 1175 + <span class="social-quote-source">VT student</span> 1176 + </div> 1177 + <div class="social-quote"> 1178 + <span class="social-quote-text">this is rad actually</span> 1179 + <span class="social-quote-source">VT student</span> 1180 + </div> 1181 + </div> 1182 + </div> 1183 + 1184 + <div class="proof-strip" aria-label="Sponsor confidence points"> 1185 + <div class="proof-item"> 1186 + <div class="proof-icon"> 1187 + <svg viewBox="0 0 15 15" fill="none" stroke="#861F41" stroke-width="1.5" stroke-linecap="round"> 1188 + <path d="M7.5 1a6.5 6.5 0 1 0 0 13A6.5 6.5 0 0 0 7.5 1z"/> 1189 + <path d="M5 7.5l2 2 3.5-3.5"/> 1190 + </svg> 1191 + </div> 1192 + <div class="proof-title">It&rsquo;s a real app, live on the App Store</div> 1193 + <div class="proof-desc">iPhone, iPad, Apple Watch, and home screen widget. It&rsquo;s been in the App Store since 2022.</div> 1194 + </div> 1195 + <div class="proof-item"> 1196 + <div class="proof-icon"> 1197 + <svg viewBox="0 0 15 15" fill="none" stroke="#861F41" stroke-width="1.5" stroke-linecap="round"> 1198 + <rect x="1" y="3" width="13" height="9" rx="1.5"/> 1199 + <path d="M4 6.5h7M4 9h4"/> 1200 + </svg> 1201 + </div> 1202 + <div class="proof-title">Ads are labeled as sponsored</div> 1203 + <div class="proof-desc">Clearly marked. No tricks.</div> 1204 + </div> 1205 + <div class="proof-item"> 1206 + <div class="proof-icon"> 1207 + <svg viewBox="0 0 15 15" fill="none" stroke="#861F41" stroke-width="1.5" stroke-linecap="round"> 1208 + <path d="M2 11l3.5-4.5 2.5 2.5 2-2.5 3 4"/> 1209 + <path d="M2 13h11"/> 1210 + </svg> 1211 + </div> 1212 + <div class="proof-title">Simple to get started</div> 1213 + <div class="proof-desc">Send your dates and what you&rsquo;d like to say. I handle setup and scheduling.</div> 1214 + </div> 1215 + </div> 1216 + </div> 1217 + </section> 1218 + 1219 + <section id="formats" class="section"> 1220 + <div class="container"> 1221 + <div class="section-header"> 1222 + <p class="section-label">Formats</p> 1223 + <h2 class="section-title">Three formats</h2> 1224 + <p class="section-sub">Text-only, banner, or feature. All appear in the main feed.</p> 1225 + </div> 1226 + <div class="formats-grid"> 1227 + <div class="format-card"> 1228 + <div class="format-card-preview"> 1229 + <div class="format-preview-text"> 1230 + <div class="fph"></div> 1231 + <div class="fps"></div> 1232 + <div class="fpc"></div> 1233 + </div> 1234 + </div> 1235 + <div class="format-card-info"> 1236 + <div class="format-card-name">Text</div> 1237 + <div class="format-card-desc">Sponsor name, headline, subline, and a CTA. No image needed.</div> 1238 + </div> 1239 + </div> 1240 + <div class="format-card"> 1241 + <div class="format-card-preview"> 1242 + <div class="format-preview-banner"> 1243 + <div class="format-preview-img"><span>Banner image</span></div> 1244 + <div class="format-preview-body"> 1245 + <div class="fph"></div> 1246 + <div class="fps"></div> 1247 + <div class="fpc"></div> 1248 + </div> 1249 + </div> 1250 + </div> 1251 + <div class="format-card-info"> 1252 + <div class="format-card-name">Banner</div> 1253 + <div class="format-card-desc">An image plus a short block of copy. Standard placement in the feed.</div> 1254 + </div> 1255 + </div> 1256 + <div class="format-card"> 1257 + <div class="format-card-preview"> 1258 + <div class="format-preview-banner format-preview-feature"> 1259 + <div class="format-preview-img"><span>Feature image</span></div> 1260 + <div class="format-preview-body"> 1261 + <div class="fph"></div> 1262 + <div class="fps"></div> 1263 + <div class="fpc"></div> 1264 + </div> 1265 + </div> 1266 + </div> 1267 + <div class="format-card-info"> 1268 + <div class="format-card-name">Feature</div> 1269 + <div class="format-card-desc">Taller image and more room for copy. The largest format available.</div> 1270 + </div> 1271 + </div> 1272 + </div> 1273 + </div> 1274 + </section> 1275 + 1276 + <section id="wizard" class="section section-alt"> 1277 + <div class="container"> 1278 + <div class="section-header"> 1279 + <p class="section-label">Ad Preview</p> 1280 + <h2 class="section-title">See what it looks like</h2> 1281 + <p class="section-sub">Fill in your details and switch between formats. Good for getting a concrete idea before you reach out.</p> 1282 + </div> 1283 + <div class="wizard-grid"> 1284 + <form id="mockupForm" class="admin-form"> 1285 + <div class="form-group"> 1286 + <label class="form-label" for="sponsor">Business / Sponsor Name</label> 1287 + <input class="form-input" type="text" id="sponsor" name="sponsor" placeholder="e.g. Benny&rsquo;s Coffee" autocomplete="off"> 1288 + </div> 1289 + <div class="form-group"> 1290 + <label class="form-label" for="headline">Headline</label> 1291 + <input class="form-input" type="text" id="headline" name="headline" placeholder="e.g. Fuel your workout" autocomplete="off"> 1292 + </div> 1293 + <div class="form-group"> 1294 + <label class="form-label" for="subline">Subline <span style="font-weight:400;color:var(--muted)">(optional)</span></label> 1295 + <input class="form-input" type="text" id="subline" name="subline" placeholder="e.g. 310 N Main St &middot; Open 7am&ndash;9pm" autocomplete="off"> 1296 + </div> 1297 + <div class="form-group"> 1298 + <label class="form-label" for="cta">Call to Action</label> 1299 + <input class="form-input" type="text" id="cta" name="cta" placeholder="e.g. View menu" autocomplete="off"> 1300 + </div> 1301 + <div class="form-group" id="imageFieldWrap"> 1302 + <label class="form-label" for="image_file">Image</label> 1303 + <div class="form-file-row"> 1304 + <input class="form-file-input" type="file" id="image_file" name="image_file" accept="image/*" aria-label="Choose image file"> 1305 + <input class="form-input" type="url" id="image_url" name="image_url" placeholder="Or paste image URL (https://)" autocomplete="off"> 1306 + </div> 1307 + <p class="form-hint">Required for Banner and Feature formats. Upload or paste URL.</p> 1308 + </div> 1309 + <div class="form-group"> 1310 + <label class="form-label" for="logo_file">Logo <span style="font-weight:400;color:var(--muted)">(optional)</span></label> 1311 + <div class="form-file-row"> 1312 + <input class="form-file-input" type="file" id="logo_file" name="logo_file" accept="image/*" aria-label="Choose logo file"> 1313 + <input class="form-input" type="url" id="logo_url" name="logo_url" placeholder="Or paste logo URL (https://)" autocomplete="off"> 1314 + </div> 1315 + </div> 1316 + <input type="hidden" id="tier" name="tier" value="banner"> 1317 + <div class="wizard-cta-box"> 1318 + <p>Ready to move forward? Send your target dates and what you want to say. I&rsquo;ll handle the rest.</p> 1319 + <a href="#contact" class="btn-contact">Get in touch &rarr;</a> 1320 + </div> 1321 + </form> 1322 + 1323 + <aside class="preview-pane"> 1324 + <div id="adPreview" class="ad-preview"><div class="preview preview-banner"> 1325 + <div class="preview-header"><span class="preview-header-sponsor">Benny&rsquo;s Coffee</span><span class="preview-header-sponsored">Sponsored</span></div> 1326 + <div class="preview-img-placeholder" style="height:140px">Image</div> 1327 + <div class="preview-copy-block"> 1328 + <div class="preview-copy"><strong class="preview-headline">Fuel your workout</strong><span class="preview-subline">310 N Main St &middot; Open 7am&ndash;9pm</span></div> 1329 + <div class="preview-cta-wrap"><span class="preview-cta-text">View menu</span><span class="preview-cta-arrow">↗</span></div> 1330 + </div> 1331 + </div></div> 1332 + <div class="tier-toggle"> 1333 + <button type="button" class="tier-btn" data-tier="text">Text</button> 1334 + <button type="button" class="tier-btn active" data-tier="banner">Banner</button> 1335 + <button type="button" class="tier-btn" data-tier="feature">Feature</button> 1336 + </div> 1337 + </aside> 1338 + </div> 1339 + </div> 1340 + </section> 1341 + 1342 + <section id="contact" class="section section-dark"> 1343 + <div class="container"> 1344 + <div class="contact-inner"> 1345 + <h2 class="section-title on-dark">Get in touch</h2> 1346 + <p class="section-sub">Send a few details and I&rsquo;ll follow up with format suggestions and next steps.</p> 1347 + <div class="contact-card"> 1348 + <div> 1349 + <h3 class="contact-col-title">What to include</h3> 1350 + <p class="contact-col-sub">Enough to give you a quick turnaround.</p> 1351 + <ul class="contact-points"> 1352 + <li>Your business name and website</li> 1353 + <li>Target dates or general campaign window</li> 1354 + <li>What you&rsquo;re trying to accomplish (awareness, event, foot traffic)</li> 1355 + <li>Format preference, if you have one&mdash;or just ask</li> 1356 + </ul> 1357 + </div> 1358 + <div class="contact-cta-col"> 1359 + <p class="contact-cta-label">Direct line</p> 1360 + <a href="mailto:hello@jackhannon.net?subject=Gym%20Tracker%20Advertising%20Inquiry" class="btn-email">Email me &rarr;</a> 1361 + <p class="contact-note">I typically reply within a day. Highest traffic windows are January&ndash;May and August&ndash;December.</p> 1362 + </div> 1363 + </div> 1364 + </div> 1365 + </div> 1366 + </section> 1367 + 1368 + <footer class="footer"> 1369 + <div class="container footer-inner"> 1370 + <span class="footer-copy">&copy; Jack Hannon</span> 1371 + <div class="footer-links"> 1372 + <a href="https://gymtracker.jackhannon.net/docs/privacy-policy.html">Privacy</a> 1373 + <a href="https://jackhannon.net/">jackhannon.net</a> 1374 + </div> 1375 + </div> 1376 + </footer> 1377 + 1378 + <script> 1379 + (function () { 1380 + var adPreview = document.getElementById('adPreview'); 1381 + var form = document.getElementById('mockupForm'); 1382 + var previewDebounce = null; 1383 + var cachedImageFile = null; 1384 + var cachedImageObjUrl = null; 1385 + var cachedLogoFile = null; 1386 + var cachedLogoObjUrl = null; 1387 + 1388 + function escapeHtml(s) { 1389 + var d = document.createElement('div'); 1390 + d.textContent = s || ''; 1391 + return d.innerHTML; 1392 + } 1393 + 1394 + function getEffectiveImageSrc() { 1395 + var fileInput = document.getElementById('image_file'); 1396 + var file = (fileInput && fileInput.files && fileInput.files[0]) ? fileInput.files[0] : null; 1397 + if (file && file.type && file.type.indexOf('image/') === 0) { 1398 + if (cachedImageFile === file && cachedImageObjUrl) return cachedImageObjUrl; 1399 + if (cachedImageObjUrl) URL.revokeObjectURL(cachedImageObjUrl); 1400 + cachedImageFile = file; 1401 + cachedImageObjUrl = URL.createObjectURL(file); 1402 + return cachedImageObjUrl; 1403 + } 1404 + if (cachedImageObjUrl) { 1405 + URL.revokeObjectURL(cachedImageObjUrl); 1406 + cachedImageObjUrl = null; 1407 + cachedImageFile = null; 1408 + } 1409 + var urlInput = document.getElementById('image_url'); 1410 + var url = urlInput ? urlInput.value.trim() : ''; 1411 + return (url && url !== 'https://') ? url : null; 1412 + } 1413 + 1414 + function getEffectiveLogoSrc() { 1415 + var fileInput = document.getElementById('logo_file'); 1416 + var file = (fileInput && fileInput.files && fileInput.files[0]) ? fileInput.files[0] : null; 1417 + if (file && file.type && file.type.indexOf('image/') === 0) { 1418 + if (cachedLogoFile === file && cachedLogoObjUrl) return cachedLogoObjUrl; 1419 + if (cachedLogoObjUrl) URL.revokeObjectURL(cachedLogoObjUrl); 1420 + cachedLogoFile = file; 1421 + cachedLogoObjUrl = URL.createObjectURL(file); 1422 + return cachedLogoObjUrl; 1423 + } 1424 + if (cachedLogoObjUrl) { 1425 + URL.revokeObjectURL(cachedLogoObjUrl); 1426 + cachedLogoObjUrl = null; 1427 + cachedLogoFile = null; 1428 + } 1429 + var urlInput = document.getElementById('logo_url'); 1430 + var url = urlInput ? urlInput.value.trim() : ''; 1431 + return (url && url !== 'https://') ? url : null; 1432 + } 1433 + 1434 + function getFormData() { 1435 + return { 1436 + sponsor: document.getElementById('sponsor').value.trim(), 1437 + headline: document.getElementById('headline').value.trim(), 1438 + subline: document.getElementById('subline').value.trim() || null, 1439 + cta: document.getElementById('cta').value.trim(), 1440 + tier: document.getElementById('tier').value, 1441 + image_src: getEffectiveImageSrc(), 1442 + logo_src: getEffectiveLogoSrc() 1443 + }; 1444 + } 1445 + 1446 + function getPlaceholder(id) { 1447 + var el = document.getElementById(id); 1448 + var p = (el && el.placeholder) || ''; 1449 + return p.replace(/^e\.g\.\s*/i, ''); 1450 + } 1451 + 1452 + function updatePreview() { 1453 + var d = getFormData(); 1454 + var tier = d.tier || 'banner'; 1455 + var sponsor = d.sponsor || getPlaceholder('sponsor'); 1456 + var headline = d.headline || getPlaceholder('headline'); 1457 + var subline = d.subline || getPlaceholder('subline'); 1458 + var cta = d.cta || getPlaceholder('cta'); 1459 + var image_src = d.image_src || null; 1460 + var logo_src = d.logo_src || null; 1461 + var usesImageLayout = tier !== 'text'; 1462 + var sponsorHeader = '<div class="preview-header">' + 1463 + (logo_src ? '<img src="' + escapeHtml(logo_src) + '" alt="" class="preview-header-logo" onerror="this.style.display=\\'none\\'">' : '') + 1464 + '<span class="preview-header-sponsor">' + escapeHtml(sponsor) + '</span>' + 1465 + '<span class="preview-header-sponsored">Sponsored</span></div>'; 1466 + var copyContent = '<div class="preview-copy">' + 1467 + '<strong class="preview-headline">' + escapeHtml(headline) + '</strong>' + 1468 + (subline ? '<span class="preview-subline">' + escapeHtml(subline) + '</span>' : '') + 1469 + '</div>'; 1470 + var ctaBtn = '<div class="preview-cta-wrap"><span class="preview-cta-text">' + escapeHtml(cta) + '</span><span class="preview-cta-arrow">↗</span></div>'; 1471 + var html = '<div class="preview preview-' + tier + '">' + sponsorHeader; 1472 + if (usesImageLayout) { 1473 + var imgHeight = tier === 'feature' ? 220 : 140; 1474 + if (image_src) { 1475 + html += '<div class="preview-img-wrap" style="height:' + imgHeight + 'px">'; 1476 + html += '<img src="' + escapeHtml(image_src) + '" alt="" class="preview-img" onerror="this.parentElement.classList.add(\\'preview-img-error\\')">'; 1477 + html += '</div>'; 1478 + } else { 1479 + html += '<div class="preview-img-placeholder" style="height:' + imgHeight + 'px">Image</div>'; 1480 + } 1481 + html += '<div class="preview-copy-block">' + copyContent + ctaBtn + '</div>'; 1482 + } else { 1483 + html += copyContent + ctaBtn; 1484 + } 1485 + html += '</div>'; 1486 + adPreview.innerHTML = html; 1487 + } 1488 + 1489 + function debouncePreview() { 1490 + clearTimeout(previewDebounce); 1491 + previewDebounce = setTimeout(updatePreview, 50); 1492 + } 1493 + 1494 + function updateTierButtons() { 1495 + var tier = document.getElementById('tier').value; 1496 + document.querySelectorAll('.tier-btn').forEach(function (btn) { 1497 + btn.classList.toggle('active', btn.dataset.tier === tier); 1498 + }); 1499 + updateImageFieldVisibility(); 1500 + } 1501 + 1502 + function updateImageFieldVisibility() { 1503 + var tier = document.getElementById('tier').value; 1504 + var wrap = document.getElementById('imageFieldWrap'); 1505 + if (wrap) wrap.classList.toggle('image-field-hidden', tier === 'text'); 1506 + } 1507 + 1508 + function run() { 1509 + var navToggle = document.getElementById('navToggle'); 1510 + var mainNav = document.getElementById('mainNav'); 1511 + var navLinks = document.getElementById('navLinks'); 1512 + if (navToggle && mainNav && navLinks) { 1513 + function closeNav() { 1514 + mainNav.classList.remove('nav-open'); 1515 + navToggle.setAttribute('aria-expanded', 'false'); 1516 + } 1517 + navToggle.addEventListener('click', function () { 1518 + var open = mainNav.classList.toggle('nav-open'); 1519 + navToggle.setAttribute('aria-expanded', open); 1520 + }); 1521 + navLinks.querySelectorAll('a').forEach(function (link) { 1522 + link.addEventListener('click', closeNav); 1523 + }); 1524 + document.addEventListener('keydown', function (e) { 1525 + if (e.key === 'Escape' && mainNav.classList.contains('nav-open')) closeNav(); 1526 + }); 1527 + } 1528 + 1529 + form.querySelectorAll('input').forEach(function (el) { 1530 + el.addEventListener('input', debouncePreview); 1531 + el.addEventListener('change', debouncePreview); 1532 + }); 1533 + 1534 + document.querySelectorAll('.tier-btn').forEach(function (btn) { 1535 + btn.addEventListener('click', function () { 1536 + document.getElementById('tier').value = btn.dataset.tier; 1537 + updateTierButtons(); 1538 + debouncePreview(); 1539 + }); 1540 + }); 1541 + 1542 + form.addEventListener('submit', function (e) { e.preventDefault(); }); 1543 + form.addEventListener('keydown', function (e) { 1544 + if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') { 1545 + e.preventDefault(); 1546 + var inputs = Array.from(form.querySelectorAll('input:not([type="hidden"])')); 1547 + var idx = inputs.indexOf(e.target); 1548 + if (idx >= 0 && idx < inputs.length - 1) { 1549 + inputs[idx + 1].focus(); 1550 + } 1551 + } 1552 + }); 1553 + 1554 + updateTierButtons(); 1555 + updatePreview(); 1556 + } 1557 + 1558 + if (document.readyState === 'loading') { 1559 + document.addEventListener('DOMContentLoaded', run); 1560 + } else { 1561 + run(); 1562 + } 1563 + }()); 1564 + </script> 1565 + </body> 1566 + </html>`; 1567 + 1568 + export function getAdsLandingHtml(): string { 1569 + return ADS_LANDING_HTML; 1570 + }
+24
workers/gymtracker-ads-api/src/index.ts
··· 249 249 } 250 250 251 251 import { getAdminHtml } from "./admin-html"; 252 + import { getAdsLandingHtml } from "./ads-landing-html"; 253 + import { getMainLandingHtml } from "./main-landing-html"; 252 254 253 255 interface PerAdStats { 254 256 ad_id: string; ··· 479 481 return jsonResponse({ deleted: id.trim() }, 200, request); 480 482 } 481 483 return jsonResponse({ error: "Method not allowed" }, 405, request); 484 + } 485 + 486 + if (url.pathname === "/" || url.pathname === "/index.html") { 487 + return new Response(getMainLandingHtml(), { 488 + headers: { 489 + "Content-Type": "text/html; charset=utf-8", 490 + "Cache-Control": isLocalRequest(request) 491 + ? "no-store, no-cache, must-revalidate" 492 + : "public, max-age=3600", 493 + }, 494 + }); 495 + } 496 + 497 + if (url.pathname === "/ads" || url.pathname === "/ads/") { 498 + return new Response(getAdsLandingHtml(), { 499 + headers: { 500 + "Content-Type": "text/html; charset=utf-8", 501 + "Cache-Control": isLocalRequest(request) 502 + ? "no-store, no-cache, must-revalidate" 503 + : "public, max-age=3600", 504 + }, 505 + }); 482 506 } 483 507 484 508 if (url.pathname !== "/api/ads") {
+114
workers/gymtracker-ads-api/src/main-landing-html.ts
··· 1 + /** Main landing page — served at /. Simple app promo with App Store badge. */ 2 + export function getMainLandingHtml(): string { 3 + return `<!DOCTYPE html> 4 + <html lang="en"> 5 + <head> 6 + <meta charset="utf-8"> 7 + <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> 8 + <title>Gym Tracker | Check McComas and War Memorial before you go</title> 9 + <meta name="description" content="Gym Tracker lets Virginia Tech students check McComas and War Memorial gym occupancy before they go."> 10 + <link rel="preconnect" href="https://fonts.googleapis.com"> 11 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 12 + <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Plus+Jakarta+Sans:wght@400;500&display=swap" rel="stylesheet"> 13 + <style> 14 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 15 + :root { 16 + --maroon: #861F41; 17 + --maroon-deep: #3a0d1e; 18 + --orange: #E87722; 19 + --text: #1a1614; 20 + --muted: #7a7270; 21 + --font-display: 'Bebas Neue', sans-serif; 22 + --font-body: 'Plus Jakarta Sans', system-ui, sans-serif; 23 + } 24 + html { scroll-behavior: smooth; } 25 + body { 26 + font-family: var(--font-body); 27 + font-size: 16px; 28 + line-height: 1.65; 29 + color: var(--text); 30 + background: var(--maroon-deep); 31 + min-height: 100vh; 32 + display: flex; 33 + flex-direction: column; 34 + -webkit-font-smoothing: antialiased; 35 + overflow-x: hidden; 36 + } 37 + .main { 38 + flex: 1; 39 + display: flex; 40 + flex-direction: column; 41 + align-items: center; 42 + justify-content: center; 43 + text-align: center; 44 + padding: clamp(32px, 8vw, 72px) clamp(24px, 5vw, 48px); 45 + max-width: 480px; 46 + margin: 0 auto; 47 + } 48 + .logo { 49 + width: 72px; 50 + height: 72px; 51 + object-fit: contain; 52 + margin-bottom: 20px; 53 + } 54 + h1 { 55 + font-family: var(--font-display); 56 + font-size: clamp(2.5rem, 8vw, 4rem); 57 + font-weight: 400; 58 + letter-spacing: 0.06em; 59 + color: #fff; 60 + line-height: 1; 61 + margin-bottom: 12px; 62 + } 63 + .tagline { 64 + font-size: 1rem; 65 + color: rgba(255,255,255,0.7); 66 + margin-bottom: 28px; 67 + line-height: 1.6; 68 + } 69 + .store-link { 70 + display: inline-block; 71 + } 72 + .store-link img { 73 + width: 165px; 74 + height: 55px; 75 + vertical-align: middle; 76 + object-fit: contain; 77 + } 78 + .footer { 79 + padding: 20px 24px; 80 + border-top: 1px solid rgba(255,255,255,0.1); 81 + display: flex; 82 + flex-wrap: wrap; 83 + justify-content: center; 84 + gap: 8px 20px; 85 + } 86 + .footer a { 87 + font-size: 0.8125rem; 88 + color: rgba(255,255,255,0.5); 89 + text-decoration: none; 90 + } 91 + .footer a:hover { 92 + color: var(--orange); 93 + } 94 + </style> 95 + </head> 96 + <body> 97 + <main class="main"> 98 + <img src="/logo.png" alt="" class="logo" width="72" height="72"> 99 + <h1>GYM TRACKER</h1> 100 + <p class="tagline">Check McComas and War Memorial before you go.</p> 101 + <a href="https://apps.apple.com/us/app/vt-gym-tracker/id6736409867?itscg=30200&itsct=apps_box_badge&mttnsubad=6736409867" class="store-link" target="_blank" rel="noopener noreferrer"> 102 + <img src="https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1737590400" alt="Download on the App Store"> 103 + </a> 104 + </main> 105 + <footer class="footer"> 106 + <a href="/ads">Advertise</a> 107 + <a href="https://tangled.jackhannon.net" target="_blank" rel="noopener noreferrer">Tangled</a> 108 + <a href="https://github.com/Hann8n/VTGymTracker" target="_blank" rel="noopener noreferrer">GitHub</a> 109 + <a href="/docs/privacy-policy.html">Privacy</a> 110 + <a href="https://jackhannon.net" target="_blank" rel="noopener noreferrer">jackhannon.net</a> 111 + </footer> 112 + </body> 113 + </html>`; 114 + }
+2 -1
workers/gymtracker-ads-api/worker-configuration.d.ts
··· 1 1 /* eslint-disable */ 2 - // Generated by Wrangler by running `wrangler types` (hash: 8c99578df02c89c9f0ebc90f133e1438) 2 + // Generated by Wrangler by running `wrangler types` (hash: e95ed6414296dc5526caac4989ab651a) 3 3 // Runtime types generated with workerd@1.20260317.1 2025-03-07 nodejs_compat 4 4 declare namespace Cloudflare { 5 5 interface GlobalProps { ··· 7 7 } 8 8 interface Env { 9 9 AD_CONFIG: KVNamespace; 10 + ASSETS: Fetcher; 10 11 POSTHOG_PROJECT_ID: "352692"; 11 12 POSTHOG_HOST: "https://us.posthog.com"; 12 13 }
+14 -1
workers/gymtracker-ads-api/wrangler.jsonc
··· 1 1 { 2 - // Gym Tracker Ads API — schedule sponsor ads for VT Gym Tracker 2 + // Gym Tracker Ads API — schedule sponsor ads for Gym Tracker 3 3 "name": "gymtracker-ads-api", 4 + "assets": { 5 + "directory": "./public", 6 + "binding": "ASSETS", 7 + "run_worker_first": ["/", "/admin*", "/ads*", "/api/*"] 8 + }, 4 9 "vars": { 5 10 "POSTHOG_PROJECT_ID": "352692", 6 11 "POSTHOG_HOST": "https://us.posthog.com" ··· 24 29 ], 25 30 "routes": [ 26 31 { 32 + "pattern": "gymtracker.jackhannon.net/", 33 + "zone_name": "jackhannon.net" 34 + }, 35 + { 27 36 "pattern": "gymtracker.jackhannon.net/api/*", 28 37 "zone_name": "jackhannon.net" 29 38 }, 30 39 { 31 40 "pattern": "gymtracker.jackhannon.net/admin*", 41 + "zone_name": "jackhannon.net" 42 + }, 43 + { 44 + "pattern": "gymtracker.jackhannon.net/ads*", 32 45 "zone_name": "jackhannon.net" 33 46 } 34 47 ]