diff --git a/.claude/memory/project_acg_website_redesign.md b/.claude/memory/project_acg_website_redesign.md index fbf93965..f0a0875a 100644 --- a/.claude/memory/project_acg_website_redesign.md +++ b/.claude/memory/project_acg_website_redesign.md @@ -49,6 +49,22 @@ stacked; needs Mike's call on which to keep (his show). Numbers ARE consistent ( 520.304.8300 — AGY's number-mismatch read was OCR error). Minor: svc calc-link wraps to its own line on rows 03/04 (text-length) — trivial `display:block` fix if wanted. Grok dead all session (no vision mode + empty text). +**v2 = the redesign, built on Fable (Mike: "use Fable for this"), NOW THE MAIN ww9 SITE (2026-07-22).** +Source `projects/acg-website/v2/` (index.html + css/v2.css + js/v2.js + assets/ incl. Grok-gen `hero-gen.png` +monsoon-Tucson + `field-gen.png` patch-panel). Premium editorial-brutalist: giant Anton hero, dark manifesto +inversion band ("WE CHARGE THE SAME WHETHER YOUR SYSTEMS BREAK OR NOT"), "THE INDEX"/"NOTHING HIDDEN" sections, +poster pricing, mono-metadata signature motif + faint grid. AGY pass-2: "bespoke, high-end studio delivery." +**Animated hero city rotator** (Mike's idea): odometer/mask roll, Tucson->Phoenix->Scottsdale->Flagstaff->EVERYWHERE +(~3.3s, runs once, underline-slab payoff; reduced-motion shows EVERYWHERE static; all 10 AZ cities stay in the +crawlable SERVICE-AREA strip + LocalBusiness `areaServed` JSON-LD for SEO — Mike wants to target beyond Tucson). +**Promoted to ww9 ROOT 2026-07-22**: copied v2 to `/home/azcomputerguru/public_html/ww9/` (index.html overwritten, +css/v2.css+js/v2.js+assets ADDED; v1 css/styles.css+js/app.js KEPT for the still-v1 subpages), rewrote v2's nav +`../page.html`->`page.html` (root-relative), added no-cache `.htaccess` (staging; 1yr max-age default was hiding +updates), backed up v1 home as `index- v1-backup.html`. The `/v2/` subfolder still exists (redundant now). +CAVEAT: only the HOMEPAGE is redesigned — about/pricing/services/contact/calculator are STILL v1 (design mismatch) +until redesigned. Repo v2/ is UNCOMMITTED. **ff.py capture gotcha (bit me twice): persistent ff-profile caches +old js/css — verify with a FRESH `FF_PROFILE` temp dir, not `?cb=` on the page URL (doesn't bust linked assets).** + **AutoSSL fix (2026-07-22):** ww9 had a self-signed cert (browser warnings) because it was in the account's AutoSSL **exclude list** (`/var/cpanel/ssl/autossl/excludes/azcomputerguru.json`, alongside proxied/external subdomains). Removed ww9+www.ww9 from excludes, triggered `uapi --user=azcomputerguru SSL start_autossl_check` diff --git a/projects/acg-website/v2/.htaccess b/projects/acg-website/v2/.htaccess new file mode 100644 index 00000000..6b0e1008 --- /dev/null +++ b/projects/acg-website/v2/.htaccess @@ -0,0 +1,9 @@ +# Staging: force revalidation so design iterations are always visible (no stale css/js). +# Replace with versioned filenames + long cache at go-live. + + + Header set Cache-Control "no-cache, no-store, must-revalidate" + Header set Pragma "no-cache" + Header unset Expires + + diff --git a/projects/acg-website/v2/IMAGE-SPEC.md b/projects/acg-website/v2/IMAGE-SPEC.md new file mode 100644 index 00000000..d9938b77 --- /dev/null +++ b/projects/acg-website/v2/IMAGE-SPEC.md @@ -0,0 +1,54 @@ +# v2 IMAGE-SPEC: AI art direction shot list + +All images render through the CSS treatment system (`.frame--duo`): grayscale source +multiplied over a signal-orange plate (ink + orange duotone), halftone dot screen, +film grain, 3px ink border, hard offset slab shadow. Generate sources as normal +color/grayscale photographs; the CSS does the treatment. High contrast and strong +shadows in the source read best after the multiply pass. No text in any image. + +Status legend: [FINAL-CANDIDATE] = a Grok-generated image is already in place and +approved-quality; [PLACEHOLDER] = still using a v1 asset, generate per spec. + +## Slot 1: Hero band (assets/hero-gen.png) [FINAL-CANDIDATE] +- Scene: monsoon storm over the Tucson valley at dusk, saguaro silhouettes in + foreground, Santa Catalina mountains, rain columns, city lights below. +- Mood: dramatic, rooted-in-place, weathered confidence. +- Composition: wide landscape, horizon in lower third, tall saguaros left of + center (page crops to 21:8 on desktop, 3:2 mobile; keep key subjects central). +- Aspect: generate 16:9 or wider, at least 1600px wide. +- Prompt used: "High-contrast editorial photograph, dramatic monsoon storm clouds + over the Tucson valley at dusk, saguaro cactus silhouettes in foreground, Santa + Catalina mountains, moody documentary style, strong shadows, no text" + +## Slot 2: Manifesto field shot (assets/field-gen.png) [FINAL-CANDIDATE] +- Scene: technician's hands terminating cat6 at a dense patch panel, neat cable + bundles, hard side light. No faces. +- Mood: hands-on, technical, honest labor. +- Composition: hands centered (page crops portrait source to 21:9; regenerate in + landscape 3:2 if a wider field of the rack is wanted). +- Prompt used: "Documentary photograph, close-up of a technician's hands + terminating ethernet cable at a dense server rack patch panel, neat cable + bundles, shallow depth of field, hard side light, gritty editorial style, + no faces, no text" + +## Slot 3 (future, services/about pages): bench repair +- Scene: open desktop tower on a workbench, screwdriver in hand, parts trays, + anti-static mat, warm workshop light through a window, Tucson desert visible + outside. +- Mood: craftsmanship, 25 years of reps. +- Aspect: 3:2 landscape, 1600px+. + +## Slot 4 (future, contact page): the shop +- Scene: single-story storefront on East 22nd St at golden hour, clean signage + band (blank, no legible text), desert landscaping, long shadows. +- Mood: local, physical, findable. +- Aspect: 3:2 landscape. + +## Slot 5 (future, dispatch/report headers): abstract technical +- Scene options: coiled patch cable macro on bone paper; rack LEDs bokeh through + a perforated door; a UPS battery row; tape/loose drives on a desk (backup story). +- Mood: evidence photography, field documentation. +- Aspect: 16:9, cropped to 21:9 in card headers. + +## Regeneration command +bash .claude/skills/grok/scripts/ask-grok.sh image "" projects/acg-website/v2/assets/.png diff --git a/projects/acg-website/v2/assets/about.png b/projects/acg-website/v2/assets/about.png new file mode 100644 index 00000000..9b6c13cb Binary files /dev/null and b/projects/acg-website/v2/assets/about.png differ diff --git a/projects/acg-website/v2/assets/acg-mark.svg b/projects/acg-website/v2/assets/acg-mark.svg new file mode 100644 index 00000000..c0f730f9 --- /dev/null +++ b/projects/acg-website/v2/assets/acg-mark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/projects/acg-website/v2/assets/field-gen.png b/projects/acg-website/v2/assets/field-gen.png new file mode 100644 index 00000000..e4051bb1 Binary files /dev/null and b/projects/acg-website/v2/assets/field-gen.png differ diff --git a/projects/acg-website/v2/assets/hero-gen.png b/projects/acg-website/v2/assets/hero-gen.png new file mode 100644 index 00000000..22acf3d1 Binary files /dev/null and b/projects/acg-website/v2/assets/hero-gen.png differ diff --git a/projects/acg-website/v2/assets/hero.png b/projects/acg-website/v2/assets/hero.png new file mode 100644 index 00000000..b815a4b2 Binary files /dev/null and b/projects/acg-website/v2/assets/hero.png differ diff --git a/projects/acg-website/v2/assets/services.png b/projects/acg-website/v2/assets/services.png new file mode 100644 index 00000000..bce7aabf Binary files /dev/null and b/projects/acg-website/v2/assets/services.png differ diff --git a/projects/acg-website/v2/assets/story.png b/projects/acg-website/v2/assets/story.png new file mode 100644 index 00000000..f958484f Binary files /dev/null and b/projects/acg-website/v2/assets/story.png differ diff --git a/projects/acg-website/v2/css/v2.css b/projects/acg-website/v2/css/v2.css new file mode 100644 index 00000000..c1f2572e --- /dev/null +++ b/projects/acg-website/v2/css/v2.css @@ -0,0 +1,730 @@ +/* =========================================================================== + ARIZONA COMPUTER GURU - v2 "Signal" + Premium brutalist, refined craft. Anton poster scale, signal orange, + duotone treated imagery, mono metadata motif, hard offset shadows. + =========================================================================== */ + +/* ---- Tokens ------------------------------------------------------------- */ +:root { + /* bone / ink neutrals, tinted toward the brand hue (hex fallback first) */ + --paper: #F4EDE1; + --surface: #FBF7F0; + --surface-2: #EAE2D4; + --ink: #16120F; + --ink-2: #46403A; + --ink-3: #5C544B; + --accent: #E24A12; /* fills only */ + --accent-ink: #C23A0A; /* orange TEXT on bone, AA 4.62:1 */ + --on-accent: #16120F; + --paper: oklch(0.944 0.017 84); + --surface: oklch(0.972 0.011 84); + --surface-2: oklch(0.912 0.022 84); + --ink: oklch(0.175 0.009 55); + --ink-2: oklch(0.37 0.012 55); + --ink-3: oklch(0.445 0.014 55); + --accent: oklch(0.585 0.185 38); + --accent-ink: oklch(0.51 0.175 38); + + --rule: color-mix(in srgb, var(--ink) 24%, transparent); + --rule-soft: color-mix(in srgb, var(--ink) 10%, transparent); + --grid-line: color-mix(in srgb, var(--ink) 7%, transparent); + + /* dark band (in-page inversion, independent of theme; literal constants + so the band stays charcoal + bone in BOTH themes) */ + --band-bg: #1A1410; + --band-bg: oklch(0.205 0.012 55); + --band-ink: #F4EDE1; + --band-ink: oklch(0.944 0.017 84); + --band-muted: #C9BFB2; + --band-muted: oklch(0.80 0.018 84); + --band-accent: #FF5A1F; + --band-accent: oklch(0.66 0.20 38); + --band-accent-ink: #FF8A4D; + --band-accent-ink: oklch(0.735 0.155 45); + --band-rule: color-mix(in srgb, #F4EDE1 18%, transparent); + + --shadow-c: var(--ink); /* hard offset shadow color */ + --border-w: 3px; + --maxw: 1240px; + + --f-display: "Anton", "Arial Narrow Bold", system-ui, sans-serif; + --f-body: "Hanken Grotesk", system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + --f-mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace; + + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* expo-ish */ + --t-fast: 90ms; + --t-med: 180ms; +} + +[data-theme="dark"] { + --paper: #0E0B09; + --surface: #171310; + --surface-2: #201A15; + --ink: #F4EDE1; + --ink-2: #C9C1B5; + --ink-3: #9A9082; + --accent: #FF5A1F; + --accent-ink: #FF8A4D; + --on-accent: #16120F; + --paper: oklch(0.155 0.008 55); + --surface: oklch(0.205 0.010 55); + --surface-2: oklch(0.245 0.012 55); + --ink: oklch(0.944 0.017 84); + --ink-2: oklch(0.82 0.016 84); + --ink-3: oklch(0.665 0.016 70); + --accent: oklch(0.66 0.20 38); + --accent-ink: oklch(0.735 0.155 45); + --on-accent: oklch(0.175 0.009 55); + + --rule: color-mix(in srgb, var(--ink) 26%, transparent); + --rule-soft: color-mix(in srgb, var(--ink) 10%, transparent); + --grid-line: color-mix(in srgb, var(--ink) 8%, transparent); + + /* the inversion band goes deeper than the page so pacing survives dark theme */ + --band-bg: #070504; + --band-bg: oklch(0.115 0.006 55); + --shadow-c: #000000; + --shadow-c: oklch(0.08 0.004 55); +} + +/* ---- Reset -------------------------------------------------------------- */ +*, *::before, *::after { box-sizing: border-box; } +* { margin: 0; } +html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; } +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + } +} +body { + background: var(--paper); + color: var(--ink); + font-family: var(--f-body); + font-size: 1rem; + line-height: 1.55; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + overflow-x: clip; +} +img { display: block; max-width: 100%; height: auto; } +a { color: var(--accent-ink); text-decoration-thickness: 1px; text-underline-offset: 3px; } +a:hover { color: var(--ink); } +::selection { background: var(--accent); color: var(--on-accent); } +.mono { font-family: var(--f-mono); font-variant-numeric: tabular-nums; } + +.sr-only { + position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; + overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; +} +.skip-link { + position: absolute; left: 0.75rem; top: -100px; z-index: 200; + background: var(--ink); color: var(--paper); font-family: var(--f-mono); + padding: 0.6rem 1rem; border: var(--border-w) solid var(--accent); + transition: top 120ms var(--ease-out); +} +.skip-link:focus-visible { top: 0.75rem; color: var(--paper); } + +a:focus-visible, button:focus-visible { + outline: 3px solid var(--accent); outline-offset: 3px; +} + +.wrap { width: min(100% - 3rem, var(--maxw)); margin-inline: auto; } + +/* ---- Header ------------------------------------------------------------- */ +.site-header { + position: sticky; top: 0; z-index: 100; + background: var(--paper); + border-bottom: var(--border-w) solid var(--ink); +} +.header-grid { + width: min(100% - 3rem, var(--maxw)); margin-inline: auto; + display: flex; align-items: center; gap: 1.5rem; + min-height: 4.25rem; +} +.brand { display: flex; align-items: center; gap: 0.7rem; text-decoration: none; color: var(--ink); } +.brand__mark { color: var(--accent); line-height: 0; } +.brand__mark svg { width: 32px; height: 33px; } +.brand__text { display: flex; flex-direction: column; line-height: 1.15; } +.brand__name { + font-family: var(--f-display); font-weight: 400; font-size: 1.15rem; + text-transform: uppercase; letter-spacing: 0.02em; +} +.brand__since { + font-family: var(--f-mono); font-size: 0.6rem; letter-spacing: 0.2em; + text-transform: uppercase; color: var(--ink-3); +} +.nav { margin-left: auto; display: flex; gap: 1.4rem; } +.nav__link { + font-family: var(--f-mono); font-size: 0.78rem; letter-spacing: 0.12em; + text-transform: uppercase; text-decoration: none; color: var(--ink-2); + padding-block: 0.35rem; position: relative; +} +.nav__link::after { + content: ""; position: absolute; left: 0; bottom: 0; height: 3px; width: 0; + background: var(--accent); transition: width var(--t-med) var(--ease-out); +} +.nav__link:hover { color: var(--ink); } +.nav__link:hover::after { width: 100%; } +.header-actions { display: flex; align-items: center; gap: 0.9rem; } +.nav__phone { + font-family: var(--f-mono); font-size: 0.88rem; font-weight: 700; + color: var(--ink); text-decoration: none; + border: var(--border-w) solid var(--ink); padding: 0.45rem 0.8rem; + transition: background var(--t-med) var(--ease-out), color var(--t-med) var(--ease-out); +} +.nav__phone:hover { background: var(--ink); color: var(--paper); } +.theme-toggle { + background: transparent; border: var(--border-w) solid var(--ink); + width: 44px; height: 44px; cursor: pointer; color: var(--ink); + display: grid; place-items: center; font-size: 1.05rem; + transition: background var(--t-med), color var(--t-med); +} +.theme-toggle:hover { background: var(--ink); color: var(--paper); } + +@media (max-width: 900px) { .nav { display: none; } } +@media (max-width: 560px) { + .brand__since { display: none; } + .nav__phone { box-shadow: none; border-width: 2px; padding: 0.4rem 0.6rem; font-size: 0.8rem; } +} + +/* ---- Frame: treated imagery system -------------------------------------- */ +/* Any image dropped into .frame--duo reads as art-directed: grayscale source + multiplied over a signal-orange plate (ink+orange duotone), halftone dot + screen, film grain, hard border + offset slab. */ +.frame { + position: relative; overflow: clip; + border: var(--border-w) solid var(--ink); + box-shadow: 12px 12px 0 var(--shadow-c); + background: var(--surface); +} +.frame--duo { background: #E24A12; background: oklch(0.585 0.185 38); } +.frame--duo img { + width: 100%; height: 100%; object-fit: cover; + filter: grayscale(1) contrast(1.28) brightness(1.06); + mix-blend-mode: multiply; +} +/* halftone screen */ +.frame--duo::before { + content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; + background-image: radial-gradient(circle, rgba(12, 8, 5, 0.5) 1px, transparent 1.4px); + background-size: 5px 5px; + mix-blend-mode: overlay; + opacity: 0.55; +} +/* film grain */ +.frame--duo::after { + content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; + background-image: url("data:image/svg+xml;utf8,"); + mix-blend-mode: overlay; opacity: 0.35; +} +.frame__meta { + position: absolute; z-index: 3; + font-size: 0.62rem; letter-spacing: 0.14em; + background: var(--ink); color: var(--paper); + padding: 0.25rem 0.5rem; +} +[data-theme="dark"] .frame__meta { background: oklch(0.115 0.006 55); color: oklch(0.944 0.017 84); } +.frame__meta--tl { top: 0; left: 0; } +.frame__meta--br { bottom: 0; right: 0; } +.frame--band { border-color: var(--band-ink); box-shadow: 12px 12px 0 var(--band-accent); } + +/* ---- Buttons ------------------------------------------------------------ */ +.btn { + font-family: var(--f-display); font-weight: 400; font-size: 1.08rem; + letter-spacing: 0.03em; text-transform: uppercase; + display: inline-flex; align-items: center; gap: 0.65rem; + padding: 0.85rem 1.6rem; cursor: pointer; text-decoration: none; + border: var(--border-w) solid var(--ink); + box-shadow: 6px 6px 0 var(--shadow-c); + transition: transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out), + background var(--t-med) var(--ease-out), color var(--t-med) var(--ease-out); +} +.btn:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--shadow-c); } +.btn:active { transform: translate(4px, 4px); box-shadow: 1px 1px 0 var(--shadow-c); } +.btn--primary { background: var(--accent); color: var(--on-accent); } +.btn--primary:hover { background: var(--ink); color: var(--paper); } +.btn--ghost { background: transparent; color: var(--ink); } +.btn--ghost:hover { background: var(--ink); color: var(--paper); } +.btn .arrow { font-family: var(--f-mono); } + +/* band variants sit on the dark inversion band */ +.btn--band { border-color: var(--band-ink); box-shadow: 6px 6px 0 var(--band-accent); background: var(--band-accent); color: #16120F; } +.btn--band:hover { background: var(--band-ink); color: var(--band-bg); box-shadow: 8px 8px 0 var(--band-accent); } +.btn--band-ghost { border-color: var(--band-ink); color: var(--band-ink); box-shadow: 6px 6px 0 var(--band-accent); background: transparent; font-family: var(--f-mono); font-weight: 700; } +.btn--band-ghost:hover { background: var(--band-ink); color: var(--band-bg); box-shadow: 8px 8px 0 var(--band-accent); } + +/* ---- Shared section furniture ------------------------------------------- */ +.section-head { position: relative; display: flex; align-items: baseline; gap: 1.2rem; margin-bottom: 1.4rem; } +.section-no { + font-family: var(--f-display); font-size: clamp(3.2rem, 7vw, 5.5rem); line-height: 0.8; + color: transparent; + -webkit-text-stroke: 2px var(--band-accent-ink); +} +.section-no--ink { -webkit-text-stroke: 2px var(--accent-ink); } +.kicker { + font-size: 0.72rem; letter-spacing: 0.22em; text-transform: uppercase; + color: var(--accent-ink); display: flex; align-items: center; gap: 0.75rem; +} +.kicker::before { content: ""; width: 30px; border-top: 3px solid var(--accent); } +.band-ink .kicker { color: var(--band-accent-ink); } +.band-ink .kicker::before { border-color: var(--band-accent); } + +.lead { font-size: 1.15rem; color: var(--ink-2); max-width: 62ch; margin-bottom: 2rem; } +.accent-word { color: var(--accent-ink); font-style: normal; } + +.more-link { + font-family: var(--f-display); font-weight: 400; font-size: 1.05rem; + letter-spacing: 0.02em; text-transform: uppercase; + color: var(--accent-ink); text-decoration: none; + display: inline-flex; gap: 0.5rem; align-items: center; + border-bottom: 3px solid var(--accent); padding-bottom: 2px; +} +.more-link:hover { color: var(--ink); border-color: var(--ink); } +.more-link .arrow { font-family: var(--f-mono); transition: transform 0.15s var(--ease-out); } +.more-link:hover .arrow { transform: translateX(4px); } +.more-link--band { color: var(--band-accent-ink); border-color: var(--band-accent); } +.more-link--band:hover { color: var(--band-ink); border-color: var(--band-ink); } + +.inline-actions { display: flex; gap: 2rem; align-items: center; flex-wrap: wrap; } + +/* visible structural grid motif */ +.grid-bg { + background-image: + repeating-linear-gradient(90deg, var(--grid-line) 0 1px, transparent 1px 120px); +} + +/* ---- Hero ---------------------------------------------------------------- */ +.hero { + position: relative; + padding: clamp(1.2rem, 3vw, 2.5rem) clamp(1rem, 3vw, 2.5rem) 0; + border-bottom: var(--border-w) solid var(--ink); + background-image: + repeating-linear-gradient(90deg, var(--grid-line) 0 1px, transparent 1px 120px); +} +.hero__rail { + position: absolute; top: 8.5rem; right: 0.4rem; + writing-mode: vertical-rl; + font-family: var(--f-mono); font-size: 0.62rem; letter-spacing: 0.34em; + text-transform: uppercase; color: var(--ink-3); + user-select: none; +} +.hero__meta { + display: flex; flex-wrap: wrap; gap: 0.55rem 2.2rem; + font-size: 0.7rem; letter-spacing: 0.18em; color: var(--ink-3); + border-bottom: 1px solid var(--rule); padding-bottom: 0.8rem; + margin-bottom: clamp(1.2rem, 3vw, 2.4rem); +} +.hero__meta-wide { margin-left: auto; color: var(--accent-ink); } +.hero__title { + font-family: var(--f-display); font-weight: 400; + text-transform: uppercase; + font-size: clamp(3.4rem, 11.6vw, 10.6rem); + line-height: 0.9; letter-spacing: 0.005em; + color: var(--ink); + margin-bottom: clamp(1.6rem, 3.5vw, 2.75rem); +} +.hero__line { display: block; } +.hero__line--hollow { + color: transparent; + -webkit-text-stroke: 4px var(--ink); + font-size: 0.62em; line-height: 0.95; + margin-top: 0.08em; +} +@media (max-width: 700px) { .hero__line--hollow { -webkit-text-stroke-width: 2px; } } +.hero__deck { + display: grid; grid-template-columns: minmax(0, 44ch) auto; + justify-content: space-between; align-items: start; + gap: 1.5rem 4rem; + border-top: 1px solid var(--rule); + padding-top: clamp(1.4rem, 2.5vw, 2rem); + padding-bottom: clamp(2.25rem, 4.5vw, 3.75rem); +} +.hero__sub { font-size: 1.22rem; color: var(--ink-2); max-width: 34ch; } +.hero__cta { display: flex; gap: 1.2rem; flex-wrap: wrap; align-items: flex-start; justify-content: flex-end; } +.hero__note { + grid-column: 1 / -1; + display: flex; flex-wrap: wrap; gap: 0.4rem 2.4rem; + font-size: 0.74rem; letter-spacing: 0.06em; color: var(--ink-3); +} +@media (max-width: 820px) { + .hero__deck { grid-template-columns: 1fr; } + .hero__cta { justify-content: flex-start; } +} +.hero__figure { margin-inline: 0; } +.hero__figure img { aspect-ratio: 21 / 8; object-fit: cover; } +.hero__figure { + margin-bottom: clamp(2rem, 4vw, 3rem); + margin-right: 12px; /* room for the offset slab */ +} +@media (max-width: 700px) { + .hero__figure img { aspect-ratio: 3 / 2; } + .hero__rail { display: none; } +} + +/* ---- Hero city rotator ---------------------------------------------------- */ +/* Words stack in one inline-grid cell: the cell is as wide as the widest word + (EVERYWHERE), so swaps cause zero layout shift. Trailing slack after short + words is invisible because the comma travels inside each word's cell and the + line ends there. + Swap = vertical odometer roll: the stack is a clipped cell (static clip-path + with small outsets so the comma, hollow stroke, and underline slab are never + sheared); words only translate. Outgoing rolls up out of the cell, incoming + rises from below, both on the SAME ease-out-expo so they travel as one rigid + column — no overshoot, no settle wobble. */ +.city-stack { display: inline-grid; clip-path: inset(-0.1em -0.15em -0.18em -0.15em); } +.city-word { grid-area: 1 / 1; white-space: nowrap; visibility: hidden; } +/* no JS: skip the cycle, rest on the payoff word */ +html:not(.js) .city-word--final { visibility: visible; } +html:not(.js) .city-word--final .city-word__t::after { transform: scaleX(1); } +.js .city-word.is-on, +.js .city-word.is-off { visibility: visible; } +.js .city-word.is-on { animation: city-roll-in 240ms cubic-bezier(0.16, 1, 0.3, 1) both; } +.js .city-word.is-off { animation: city-roll-out 240ms cubic-bezier(0.16, 1, 0.3, 1) both; } +@keyframes city-roll-in { + from { transform: translateY(1.15em); } + to { transform: none; } +} +@keyframes city-roll-out { + from { transform: none; } + to { transform: translateY(-1.15em); } +} +/* EVERYWHERE lands hollow (brief outline flourish), then rests solid with a + permanent accent underline slab drawing left to right */ +.city-word--final { transition: color 240ms var(--ease-out); } +.city-word--final.is-hollow { color: transparent; -webkit-text-stroke: 4px var(--accent); } +@media (max-width: 700px) { .city-word--final.is-hollow { -webkit-text-stroke-width: 2px; } } +.city-word__t { position: relative; display: inline-block; } +.city-word__t::after { + content: ""; position: absolute; left: 0.02em; right: 0.02em; bottom: -0.03em; + height: 0.06em; background: var(--accent); + transform: scaleX(0); transform-origin: left; +} +.city-word--final.is-settled .city-word__t::after { + transform: scaleX(1); + transition: transform 560ms var(--ease-out) 80ms; +} +/* EVERYWHERE is 4 glyphs longer than TUCSON: the city line runs a hair tighter + than line 1 (11vw vs 11.6vw, invisible side by side) so "for everywhere," + holds one line at laptop widths, and steps down on narrow viewports */ +.hero__line--city { font-size: clamp(3.4rem, 11vw, 10.6rem); } +@media (max-width: 540px) { .hero__line--city { font-size: 0.7em; } } + +/* ---- Service area strip ---------------------------------------------------- */ +.svc-area { + background: var(--surface); + border-bottom: var(--border-w) solid var(--ink); + padding: 0.95rem clamp(1rem, 3vw, 2.5rem) 1.05rem; + display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem 1.7rem; +} +.svc-area__label { + font-size: 0.7rem; font-weight: 700; letter-spacing: 0.22em; + text-transform: uppercase; color: var(--accent-ink); +} +.svc-area__list { + list-style: none; padding: 0; + display: flex; flex-wrap: wrap; gap: 0.3rem 0; + font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; + color: var(--ink-2); +} +.svc-area__list li { white-space: nowrap; } +.svc-area__list li + li::before { content: "/"; margin-inline: 0.6rem; color: var(--ink-3); } +.svc-area__wide { color: var(--accent-ink); font-weight: 700; } +.svc-area__note { + flex-basis: 100%; + font-size: 0.84rem; color: var(--ink-3); max-width: 76ch; +} + +/* ---- Radio ticker -------------------------------------------------------- */ +.ticker { + background: var(--ink); color: var(--paper); + border-bottom: var(--border-w) solid var(--accent); + overflow: clip; padding-block: 0.85rem; +} +[data-theme="dark"] .ticker { background: oklch(0.115 0.006 55); } +.ticker__track { + display: flex; width: max-content; + animation: ticker-scroll 32s linear infinite; +} +.ticker:hover .ticker__track { animation-play-state: paused; } +.ticker__item { + font-family: var(--f-mono); font-size: 0.78rem; letter-spacing: 0.12em; + text-transform: uppercase; white-space: nowrap; padding-inline: 2.6rem; + color: var(--paper); +} +[data-theme="dark"] .ticker__item { color: oklch(0.944 0.017 84); } +.ticker__item strong { color: #FF8A4D; color: oklch(0.735 0.155 45); font-weight: 700; } +.ticker__item::before { content: "\25CF"; color: var(--accent); margin-right: 1.1rem; } +@keyframes ticker-scroll { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +@media (prefers-reduced-motion: reduce) { + .ticker__track { animation: none; } +} + +/* ---- Trust stats ---------------------------------------------------------- */ +.trust { border-bottom: var(--border-w) solid var(--ink); background: var(--surface); } +.trust__grid { + display: grid; grid-template-columns: repeat(4, 1fr); +} +.trust__cell { + position: relative; + padding: clamp(2rem, 4vw, 3.4rem) clamp(1.1rem, 2.2vw, 2rem) clamp(1.8rem, 3.2vw, 2.9rem); + border-right: var(--border-w) solid var(--ink); +} +.trust__cell:last-child { border-right: 0; } +.trust__num { + font-family: var(--f-display); font-weight: 400; + font-size: clamp(2.6rem, 5.6vw, 4.9rem); line-height: 0.9; + letter-spacing: 0.01em; +} +.trust__num .u { font-size: 0.42em; color: var(--accent-ink); letter-spacing: 0.06em; } +.trust__label { font-size: 0.88rem; color: var(--ink-2); margin-top: 0.7rem; max-width: 18ch; } +.trust__tag { + position: absolute; top: 0.55rem; right: 0.7rem; + font-size: 0.6rem; letter-spacing: 0.16em; color: var(--ink-3); +} +@media (max-width: 760px) { + .trust__grid { grid-template-columns: 1fr 1fr; } + .trust__cell:nth-child(2) { border-right: 0; } + .trust__cell:nth-child(-n+2) { border-bottom: var(--border-w) solid var(--ink); } +} + +/* ---- Dark inversion band -------------------------------------------------- */ +.band-ink { + background: var(--band-bg); + color: var(--band-ink); + border-bottom: var(--border-w) solid var(--accent); + background-image: + repeating-linear-gradient(90deg, color-mix(in srgb, var(--band-ink) 6%, transparent) 0 1px, transparent 1px 120px); +} +.band-wrap { width: min(100% - 3rem, var(--maxw)); margin-inline: auto; } + +/* Manifesto */ +.manifesto { padding-block: clamp(5rem, 11vw, 10rem) clamp(2.5rem, 5vw, 4.5rem); } +.manifesto__title { + font-family: var(--f-display); font-weight: 400; text-transform: uppercase; + font-size: clamp(2.5rem, 7.4vw, 6.7rem); + line-height: 0.92; letter-spacing: 0.005em; + color: var(--band-ink); + margin-bottom: clamp(1.75rem, 4vw, 3rem); + max-width: 14em; +} +.manifesto__title span { display: block; } +.manifesto__title .accent-word { color: var(--band-accent-ink); } +.manifesto__body { + display: grid; gap: 1.4rem; max-width: 62ch; + font-size: 1.12rem; color: var(--band-muted); + margin-bottom: clamp(2.5rem, 5vw, 4rem); + justify-items: start; +} +.manifesto__punch { + font-weight: 700; color: var(--band-ink); font-size: 1.22rem; + border-block: 3px solid var(--band-accent); + padding-block: 1rem; max-width: 46ch; +} +.manifesto__figure { max-width: 900px; margin-right: 12px; } +.manifesto__figure img { aspect-ratio: 21 / 9; object-fit: cover; } + +/* Services index (inside the band) */ +.services { padding-block: clamp(1.5rem, 3vw, 2.5rem) clamp(3.5rem, 7vw, 6rem); } +.services__title { + font-family: var(--f-display); font-weight: 400; text-transform: uppercase; + font-size: clamp(2.6rem, 6vw, 5.2rem); line-height: 0.92; + color: var(--band-ink); margin-bottom: 2rem; +} +.svc-index { border-top: 1px solid var(--band-rule); margin-bottom: 2.25rem; } +.svc { + display: grid; grid-template-columns: 4.5rem 1fr auto; + gap: 1.4rem; align-items: baseline; + padding-block: 1.15rem; + border-bottom: 1px solid var(--band-rule); + transition: background var(--t-med) var(--ease-out), padding-left var(--t-med) var(--ease-out); +} +.svc:hover { background: color-mix(in srgb, var(--band-ink) 5%, transparent); } +.svc__no { + font-family: var(--f-display); font-size: 2rem; line-height: 1; + color: var(--band-accent-ink); +} +.svc__name { + font-family: var(--f-display); font-weight: 400; font-size: clamp(1.35rem, 2.4vw, 1.9rem); + letter-spacing: 0.015em; text-transform: uppercase; color: var(--band-ink); +} +.svc__desc { color: var(--band-muted); font-size: 0.96rem; margin-top: 0.3rem; max-width: 58ch; } +.svc__meta { + font-size: 0.92rem; font-weight: 700; color: var(--band-accent-ink); + text-align: right; white-space: nowrap; +} +.svc__per { color: var(--band-muted); font-weight: 400; font-size: 0.8em; } +@media (max-width: 680px) { + .svc { grid-template-columns: 2.6rem 1fr; } + .svc__no { font-size: 1.4rem; } + .svc__meta { grid-column: 2; text-align: left; } +} + +/* ---- Pricing -------------------------------------------------------------- */ +.pricing { + padding-block: clamp(4.5rem, 9vw, 8rem) clamp(3.5rem, 7vw, 6.5rem); + border-bottom: var(--border-w) solid var(--ink); +} +.pricing__title { + font-family: var(--f-display); font-weight: 400; text-transform: uppercase; + font-size: clamp(3rem, 8.5vw, 7.6rem); line-height: 0.88; + margin-bottom: 1.5rem; +} +.cta__accent { color: var(--band-accent-ink); } + +.tiers { + display: grid; grid-template-columns: repeat(3, 1fr); + gap: clamp(1.5rem, 3vw, 2.5rem); + align-items: stretch; + margin-bottom: clamp(2.5rem, 5vw, 3.5rem); + padding-right: 14px; /* room for offset slabs */ +} +.tier { + position: relative; + background: var(--surface); + border: 4px solid var(--ink); + box-shadow: 10px 10px 0 var(--shadow-c); + padding: 2rem 1.6rem 1.8rem; + transition: transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out); +} +.tier:hover { transform: translate(-3px, -3px); box-shadow: 14px 14px 0 var(--shadow-c); } +.tier--pop { + background: var(--ink); color: var(--paper); + box-shadow: 10px 10px 0 var(--accent); +} +.tier--pop:hover { box-shadow: 14px 14px 0 var(--accent); } +[data-theme="dark"] .tier--pop { background: oklch(0.115 0.006 55); border-color: var(--accent); color: var(--ink); } +.tier__flag { + position: absolute; top: -4px; right: -4px; + font-size: 0.66rem; letter-spacing: 0.18em; text-transform: uppercase; + background: var(--accent); color: var(--on-accent); + padding: 0.4rem 0.7rem; +} +.tier__name { + font-family: var(--f-display); font-weight: 400; font-size: 1.5rem; + letter-spacing: 0.02em; text-transform: uppercase; +} +.tier__price { + font-family: var(--f-display); font-weight: 400; + font-size: clamp(4.4rem, 8vw, 6.8rem); line-height: 0.9; + margin-top: 1rem; letter-spacing: 0.01em; +} +.tier__cur { font-size: 0.45em; vertical-align: 0.75em; color: var(--accent-ink); } +.tier--pop .tier__cur { color: #FF8A4D; color: oklch(0.735 0.155 45); } +.tier__per { font-size: 0.72rem; letter-spacing: 0.16em; text-transform: uppercase; + color: var(--ink-3); margin-top: 0.7rem; } +.tier--pop .tier__per { color: oklch(0.80 0.018 84); } +.tier__blurb { font-size: 0.95rem; color: var(--ink-2); margin-top: 1.1rem; max-width: 30ch; } +.tier--pop .tier__blurb { color: oklch(0.80 0.018 84); } +.tier__tag { position: absolute; bottom: 0.6rem; right: 0.75rem; font-size: 0.6rem; + letter-spacing: 0.16em; color: var(--ink-3); } +.tier--pop .tier__tag { color: oklch(0.665 0.016 70); } +@media (max-width: 860px) { + .tiers { grid-template-columns: 1fr; max-width: 460px; } +} + +/* ---- Dispatch board -------------------------------------------------------- */ +.dispatch { + padding-block: clamp(3.5rem, 7vw, 6rem); + background: var(--surface); + border-bottom: var(--border-w) solid var(--ink); +} +.dispatch__title { + font-family: var(--f-display); font-weight: 400; text-transform: uppercase; + font-size: clamp(2.4rem, 5.5vw, 4.6rem); line-height: 0.92; + margin-bottom: 1.25rem; +} +.dispatch-grid { + display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: clamp(1.4rem, 2.5vw, 2.2rem); + margin-bottom: 2.5rem; + padding-right: 10px; +} +/* flat by design: the slab shadows are reserved for buttons, tiers, and frames + so the page keeps contrast between flatness and sudden depth */ +.report { + background: var(--paper); + border: var(--border-w) solid var(--ink); + display: flex; flex-direction: column; + transition: transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out); +} +.report:hover { transform: translate(-3px, -3px); box-shadow: 6px 6px 0 var(--shadow-c); } +.report__bar { + display: flex; justify-content: space-between; gap: 0.8rem; + font-size: 0.64rem; letter-spacing: 0.16em; + background: var(--ink); color: var(--paper); + padding: 0.65rem 1rem; +} +[data-theme="dark"] .report__bar { background: oklch(0.115 0.006 55); color: var(--ink); } +.report__title { + font-family: var(--f-display); font-weight: 400; font-size: 1.45rem; + line-height: 1.08; letter-spacing: 0.01em; + padding: 1.4rem 1.2rem 0.7rem; +} +.report__excerpt { color: var(--ink-2); font-size: 0.94rem; padding: 0 1.2rem 1.5rem; flex: 1; } +.report__meta { + display: flex; justify-content: space-between; align-items: center; + font-size: 0.7rem; color: var(--ink-3); + border-top: 1px solid var(--rule); + padding: 0.7rem 1.2rem; +} +.report__tag { + background: var(--accent); color: var(--on-accent); + padding: 0.2rem 0.55rem; letter-spacing: 0.06em; text-transform: uppercase; + font-size: 0.64rem; +} + +/* ---- CTA band --------------------------------------------------------------- */ +.band-ink--cta { border-bottom: 0; } +.cta { padding-block: clamp(4rem, 9vw, 7.5rem); } +.cta .kicker { margin-bottom: 1.6rem; } +.cta__title { + font-family: var(--f-display); font-weight: 400; text-transform: uppercase; + font-size: clamp(3.2rem, 10.5vw, 9.5rem); line-height: 0.88; + color: var(--band-ink); + margin-bottom: 1.8rem; +} +.cta__title span { display: block; } +.cta__sub { color: var(--band-muted); font-size: 1.15rem; max-width: 52ch; margin-bottom: 2.4rem; } +.cta__actions { display: flex; gap: 1.4rem; flex-wrap: wrap; } + +/* ---- Footer ------------------------------------------------------------------ */ +.site-footer { + background: var(--paper); + border-top: var(--border-w) solid var(--ink); + padding-block: 2.5rem 2rem; +} +.footer-grid { + display: grid; grid-template-columns: repeat(3, auto); + justify-content: space-between; gap: 2rem; +} +.footer-brand { + font-family: var(--f-display); font-weight: 400; font-size: 1.2rem; + text-transform: uppercase; letter-spacing: 0.02em; margin-bottom: 0.5rem; +} +.footer-line { font-size: 0.76rem; color: var(--ink-3); letter-spacing: 0.04em; line-height: 1.9; } +.footer-line a { color: var(--ink-2); } +.footer-line a:hover { color: var(--accent-ink); } +.footer-col--meta { text-align: right; } +.disclaimer { + grid-column: 1 / -1; + font-size: 0.68rem; color: var(--ink-3); letter-spacing: 0.02em; + border-top: 1px dashed var(--rule); padding-top: 1rem; margin-top: 0.5rem; + max-width: 90ch; +} +@media (max-width: 760px) { + .footer-grid { grid-template-columns: 1fr; } + .footer-col--meta { text-align: left; } +} + +/* ---- Reveal on scroll (JS-gated; content never stuck hidden without JS) ---- */ +.js .reveal { + opacity: 0; transform: translateY(16px); + transition: opacity 0.65s var(--ease-out), transform 0.65s var(--ease-out); +} +.js .reveal.in { opacity: 1; transform: none; } diff --git a/projects/acg-website/v2/index.html b/projects/acg-website/v2/index.html new file mode 100644 index 00000000..c3004158 --- /dev/null +++ b/projects/acg-website/v2/index.html @@ -0,0 +1,343 @@ + + + + + + Arizona Computer Guru | Concierge IT for Tucson, Phoenix & all of Arizona + + + + + + + + + + + + + + + +
+ + +
+ +

+ TUCSON_HQ / AZ_WIDE + 32.2226N 110.9747W + EST.2001 + MANAGED IT + SECURITY +

+

+ Concierge IT for Arizona and beyond, every number on the table. + + + +

+
+

We go further than your last IT company, build a real relationship, and never hide the math. Honest pricing, local people, problems fixed before you feel them.

+ +

// month-to-month// no lock-in// no offshore call centers

+
+
+ Monsoon light over the Tucson valley, saguaro silhouettes against the Santa Catalina mountains. + + +
+
+ + +
+

Service area //

+
    +
  • Tucson
  • +
  • Phoenix
  • +
  • Mesa
  • +
  • Scottsdale
  • +
  • Chandler
  • +
  • Tempe
  • +
  • Oro Valley
  • +
  • Marana
  • +
  • Sierra Vista
  • +
  • Flagstaff
  • +
  • All of Arizona
  • +
  • Remote: everywhere
  • +
+

Headquartered in Tucson since 2001 — on-site across southern Arizona and the Phoenix metro, remote support anywhere your business runs.

+
+ + + + + +
+
+
+
2001
+
Serving Arizona businesses since
+ +
+
+
1–2HR
+
Typical onsite emergency response
+ +
+
+
100%
+
Local team, never offshore
+ +
+
+
30DAY
+
Cancel anytime, no penalty
+ +
+
+
+ + +
+
+
+
+ +

Our difference / what concierge actually means

+
+

+ We charge the same + whether your systems + break or not. +

+
+

Most IT companies wait for things to break, route you to a call center three time zones away, and lock you into a three-year contract. We built Arizona Computer Guru to be the opposite. We learn your business, pick up the phone, and explain things in plain English.

+

Because we charge the same whether your systems break or not, our only incentive is to keep them running.

+ Read our story +
+
+ A technician's hands terminating network cable at a dense patch panel. + + +
+
+
+ +
+
+
+ +

Everything you need / one local partner

+
+

The index

+
+
+ +
+

Managed IT (GPS)

+

24/7 monitoring, automated patching, and a help desk that knows your name.

+
+
from $19/endpoint
+
+
+ +
+

Cybersecurity

+

EDR, email security, dark-web monitoring, and training that catch what antivirus misses.

+
+
in GPS-Pro & up
+
+
+ +
+

Backup & Recovery

+

Tested backups, offsite copies, and ransomware rollback.

+
+
in GPS-Pro & up
+
+
+ +
+

Microsoft 365 & Email

+

Migrations, licensing, and email that is configured, secured, and supported.

+
+
from $2/mailbox
+
+
+ +
+

Business Phones

+

Cloud phone systems with mobile and desktop apps, porting, and no usage surprises.

+
+
from $22/user
+
+
+ +
+

Web & Email Hosting

+

Managed hosting with free SSL, daily backups, and real humans on support.

+
+
from $15/mo
+
+
+ See all services +
+
+
+ + +
+
+
+ +

Published, not "call for quote" / per endpoint

+
+

Nothing
hidden.

+

You pay for exactly the computers you have. No rounding you up into a bigger package tier.

+
+
+

GPS-Basic

+
19
+
per endpoint / mo
+

Essential monitoring for small, simple environments.

+ +
+
+
Most chosen
+

GPS-Pro

+
26
+
per endpoint / mo
+

Comprehensive protection for growing businesses.

+ +
+
+

GPS-Advanced

+
39
+
per endpoint / mo
+

Enterprise-grade security and compliance.

+ +
+
+ +
+
+ + +
+
+
+ +

Field intelligence / community blog

+
+

Dispatch board

+

Real fixes, real lessons, real clients. What we learned this week so you do not have to.

+
+
+
RPT-142BACKUP+RECOVERY14.2KB
+

Why Your Backup Is Not Actually a Backup

+

Three Tucson businesses discovered their backups were corrupted only after ransomware hit. Here is what they missed.

+
2026-06-14Backup & Recovery
+
+
+
RPT-141EMAIL-SEC11.8KB
+

The $47,000 Phishing Email

+

How a single wire transfer approval almost bankrupted a local law firm, and the one setting that would have stopped it.

+
2026-06-11Email Security
+
+
+
RPT-140M36512.5KB
+

Microsoft Copilot Is Leaking Your Data

+

Default Copilot settings expose internal emails and documents to the entire company. Here is how to lock it down.

+
2026-06-07Microsoft 365
+
+
+ Read all field reports +
+
+ + +
+
+
+

No pressure / no pitch deck / no countdown timer

+

+ Let's talk, + no pitch. +

+

Tell us what is frustrating you, and we'll give you honest feedback. Even if that is "your current setup is fine, here is one thing to fix."

+ +
+
+
+
+ +
+ +
+ + + diff --git a/projects/acg-website/v2/js/v2.js b/projects/acg-website/v2/js/v2.js new file mode 100644 index 00000000..a2b83095 --- /dev/null +++ b/projects/acg-website/v2/js/v2.js @@ -0,0 +1,113 @@ +/* =========================================================================== + ACG v2 homepage. Theme toggle + robust reveal-on-scroll. + =========================================================================== */ +(function () { + "use strict"; + + var $ = function (s, c) { return (c || document).querySelector(s); }; + var $$ = function (s, c) { return Array.prototype.slice.call((c || document).querySelectorAll(s)); }; + + /* ---- Theme ------------------------------------------------------------ */ + var root = document.documentElement; + var toggle = $("#themeToggle"); + var icon = $("[data-theme-icon]"); + var STORE = "acg-theme"; + + function applyTheme(mode) { + root.setAttribute("data-theme", mode); + var dark = mode === "dark"; + if (toggle) { + toggle.setAttribute("aria-pressed", String(dark)); + toggle.setAttribute("aria-label", dark ? "Switch to light theme" : "Switch to dark theme"); + } + if (icon) icon.innerHTML = dark ? "☾" : "☀"; + } + + var saved = null; + try { saved = localStorage.getItem(STORE); } catch (e) {} + var prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; + applyTheme(saved || (prefersDark ? "dark" : "light")); + + if (toggle) { + toggle.addEventListener("click", function () { + var next = root.getAttribute("data-theme") === "dark" ? "light" : "dark"; + applyTheme(next); + try { localStorage.setItem(STORE, next); } catch (e) {} + }); + } + if (window.matchMedia) { + var mq = window.matchMedia("(prefers-color-scheme: dark)"); + var onChange = function (e) { + var explicit = null; + try { explicit = localStorage.getItem(STORE); } catch (err) {} + if (!explicit) applyTheme(e.matches ? "dark" : "light"); + }; + if (mq.addEventListener) mq.addEventListener("change", onChange); + else if (mq.addListener) mq.addListener(onChange); + } + + /* ---- Hero city rotator: home, statewide, then EVERYWHERE --------------- */ + /* Runs once on load and stops on the final word. Words are real DOM text + (crawlable); JS only toggles visibility classes. Reduced motion or no JS + rests on EVERYWHERE with no cycle. */ + var cityStack = $("#cityStack"); + if (cityStack) { + var cities = $$(".city-word", cityStack); + var lastCity = cities.length - 1; + var reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + var settleFinal = function () { + cities[lastCity].classList.remove("is-hollow"); + cities[lastCity].classList.add("is-settled"); + }; + + if (reduceMotion || cities.length < 2) { + cities.forEach(function (w, i) { + w.classList.toggle("is-on", i === lastCity); + w.classList.remove("is-off", "is-hollow"); + }); + settleFinal(); + } else { + /* dwell per word before the next swap; opens on home, tightens through + the state, breathes before the payoff */ + var holds = [950, 650, 600, 700]; + var idx = 0; + var step = function () { + var out = cities[idx]; + idx += 1; + var inw = cities[idx]; + out.classList.remove("is-on"); + out.classList.add("is-off"); + if (idx === lastCity) inw.classList.add("is-hollow"); /* land hollow */ + inw.classList.add("is-on"); + window.setTimeout(function () { out.classList.remove("is-off"); }, 260); + if (idx === lastCity) { + window.setTimeout(settleFinal, 550); /* flourish, then rest solid */ + } else { + window.setTimeout(step, holds[idx] || 700); + } + }; + window.setTimeout(step, holds[0]); + } + } + + /* ---- Dynamic year ------------------------------------------------------ */ + var yr = $("#year"); + if (yr) yr.textContent = String(new Date().getFullYear()); + + /* ---- Reveal on scroll (robust: fallback reveals everything) ------------ */ + var reveals = $$(".reveal"); + if ("IntersectionObserver" in window && reveals.length) { + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (en) { + if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); } + }); + }, { rootMargin: "0px 0px -6% 0px", threshold: 0.06 }); + reveals.forEach(function (el, i) { + el.style.transitionDelay = Math.min((i % 6) * 60, 300) + "ms"; + io.observe(el); + }); + } else { + reveals.forEach(function (el) { el.classList.add("in"); }); + } +})();