Merge pull request #1 from jasonMPM/claude/isp-website-docker-EB8pB

Add Dockerized LAMP stack and website skeleton for ALWISP
This commit is contained in:
jasonMPM
2026-02-28 15:41:04 -06:00
committed by GitHub
18 changed files with 1469 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.env
docker/apache/ssl/
docker/mysql/data/

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM php:8.2-apache
# Install PHP extensions and utilities
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg-dev \
libwebp-dev \
libzip-dev \
zip \
unzip \
&& docker-php-ext-configure gd --with-jpeg --with-webp \
&& docker-php-ext-install gd pdo pdo_mysql mysqli zip \
&& rm -rf /var/lib/apt/lists/*
# Enable Apache modules
RUN a2enmod rewrite ssl headers deflate expires
# Copy PHP config
COPY docker/php/php.ini /usr/local/etc/php/conf.d/custom.ini
# Set working directory
WORKDIR /var/www/html
EXPOSE 80 443

64
docker-compose.yml Normal file
View File

@@ -0,0 +1,64 @@
version: '3.9'
services:
web:
build:
context: .
dockerfile: Dockerfile
container_name: alwisp_web
ports:
- "80:80"
- "443:443"
volumes:
- ./www:/var/www/html
- ./docker/apache/000-default.conf:/etc/apache2/sites-available/000-default.conf
- ./docker/apache/ssl:/etc/apache2/ssl
environment:
- DB_HOST=db
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
depends_on:
- db
networks:
- alwisp_net
restart: unless-stopped
db:
image: mysql:8.0
container_name: alwisp_db
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
volumes:
- db_data:/var/lib/mysql
- ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- alwisp_net
restart: unless-stopped
phpmyadmin:
image: phpmyadmin:latest
container_name: alwisp_pma
ports:
- "8080:80"
environment:
PMA_HOST: db
PMA_USER: ${DB_USER}
PMA_PASSWORD: ${DB_PASS}
depends_on:
- db
networks:
- alwisp_net
restart: unless-stopped
profiles:
- tools
volumes:
db_data:
networks:
alwisp_net:
driver: bridge

View File

@@ -0,0 +1,35 @@
<VirtualHost *:80>
ServerAdmin webmaster@alwisp.net
DocumentRoot /var/www/html
<Directory /var/www/html>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json
</IfModule>
# Cache static assets
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType text/css "access plus 1 week"
ExpiresByType application/javascript "access plus 1 week"
</IfModule>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

23
docker/mysql/init.sql Normal file
View File

@@ -0,0 +1,23 @@
-- ALWISP Database Schema (skeleton)
CREATE DATABASE IF NOT EXISTS `alwisp` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `alwisp`;
-- Contact form submissions
CREATE TABLE IF NOT EXISTS `contacts` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(120) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`phone` VARCHAR(30),
`subject` VARCHAR(255),
`message` TEXT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Service availability zones (placeholder)
CREATE TABLE IF NOT EXISTS `coverage_zones` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`zone_name` VARCHAR(120) NOT NULL,
`description` TEXT,
`active` TINYINT(1) DEFAULT 1,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

22
docker/php/php.ini Normal file
View File

@@ -0,0 +1,22 @@
; Performance
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
; Security
expose_php = Off
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
; Limits
upload_max_filesize = 32M
post_max_size = 32M
max_execution_time = 60
memory_limit = 256M
; Session
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1

8
www/.htaccess Normal file
View File

@@ -0,0 +1,8 @@
Options -Indexes
RewriteEngine On
# Route all requests through index.php (front controller)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?path=$1 [QSA,L]

707
www/css/style.css Normal file
View File

@@ -0,0 +1,707 @@
/* ============================================================
ALWISP Mesh Network Solutions
Brand palette extracted from logo:
--navy: #0d1b3e (darkest bg)
--blue: #1565c0 (primary mid)
--teal: #00bcd4 (accent highlight)
--orange: #f57c00 (mesh node accent)
--white: #ffffff
============================================================ */
/* ── RESET & BASE ──────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
/* Brand */
--navy: #0d1b3e;
--navy-mid: #132145;
--blue: #1565c0;
--blue-light:#1e88e5;
--teal: #00bcd4;
--teal-dark: #00acc1;
--orange: #f57c00;
--orange-lt: #ffb74d;
/* Neutrals */
--white: #ffffff;
--off-white: #f0f4f8;
--gray-100: #e8edf3;
--gray-300: #b0bec5;
--gray-500: #607d8b;
--gray-700: #37474f;
--gray-900: #1a2332;
/* Gradients */
--grad-hero: linear-gradient(135deg, #0d1b3e 0%, #1565c0 60%, #00bcd4 100%);
--grad-text: linear-gradient(90deg, #00bcd4, #1565c0);
--grad-cta: linear-gradient(135deg, #1565c0, #00bcd4);
/* Typography */
--font-body: 'Inter', system-ui, sans-serif;
--font-heading: 'Space Grotesk', system-ui, sans-serif;
/* Layout */
--max-width: 1200px;
--section-py: 5rem;
--nav-h: 70px;
/* Radius */
--r-sm: 6px;
--r-md: 12px;
--r-lg: 20px;
--r-full: 9999px;
/* Shadow */
--shadow-sm: 0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.08);
--shadow-md: 0 4px 16px rgba(0,0,0,.18);
--shadow-lg: 0 10px 40px rgba(0,0,0,.28);
/* Transition */
--t-fast: 0.18s ease;
--t-base: 0.28s ease;
}
html { scroll-behavior: smooth; font-size: 16px; }
body {
font-family: var(--font-body);
background: var(--navy);
color: var(--white);
line-height: 1.65;
-webkit-font-smoothing: antialiased;
}
a { color: var(--teal); text-decoration: none; transition: color var(--t-fast); }
a:hover { color: var(--orange-lt); }
img { max-width: 100%; display: block; }
ul { list-style: none; }
/* ── UTILITIES ──────────────────────────────────────────────── */
.container {
width: 100%;
max-width: var(--max-width);
margin-inline: auto;
padding-inline: 1.5rem;
}
.gradient-text {
background: var(--grad-text);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.accent { color: var(--teal); }
.section { padding-block: var(--section-py); }
.section--alt { background: var(--navy-mid); }
.section__header { text-align: center; margin-bottom: 3rem; }
.section__eyebrow {
font-size: .8rem;
font-weight: 600;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--teal);
display: block;
margin-bottom: .5rem;
}
.section__heading {
font-family: var(--font-heading);
font-size: clamp(1.75rem, 3.5vw, 2.75rem);
font-weight: 700;
line-height: 1.2;
color: var(--white);
}
.section__sub {
margin-top: .75rem;
color: var(--gray-300);
font-size: 1.05rem;
max-width: 600px;
margin-inline: auto;
}
.placeholder-block {
background: rgba(255,255,255,.05);
border: 1px dashed rgba(255,255,255,.15);
border-radius: var(--r-md);
padding: 3rem 2rem;
text-align: center;
color: var(--gray-300);
font-size: .95rem;
}
/* ── BUTTONS ────────────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .75rem 1.75rem;
border-radius: var(--r-full);
font-family: var(--font-heading);
font-size: .95rem;
font-weight: 600;
cursor: pointer;
border: 2px solid transparent;
transition: all var(--t-base);
white-space: nowrap;
}
.btn--primary {
background: var(--grad-cta);
color: var(--white);
box-shadow: 0 4px 20px rgba(0,188,212,.3);
}
.btn--primary:hover {
color: var(--white);
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(0,188,212,.45);
}
.btn--ghost {
background: transparent;
border-color: rgba(255,255,255,.35);
color: var(--white);
}
.btn--ghost:hover {
border-color: var(--teal);
color: var(--teal);
background: rgba(0,188,212,.08);
}
/* ── NAV ────────────────────────────────────────────────────── */
.site-header {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
height: var(--nav-h);
background: rgba(13,27,62,.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255,255,255,.07);
transition: background var(--t-base), box-shadow var(--t-base);
}
.site-header.scrolled {
background: rgba(13,27,62,.97);
box-shadow: var(--shadow-md);
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.nav__logo {
display: flex;
align-items: center;
gap: .6rem;
text-decoration: none;
}
.nav__logo-img { height: 38px; width: auto; }
.nav__logo-text {
font-family: var(--font-heading);
font-size: 1.35rem;
font-weight: 700;
color: var(--white);
letter-spacing: .04em;
}
.nav__menu {
display: flex;
align-items: center;
gap: .25rem;
}
.nav__link {
display: block;
padding: .5rem .9rem;
border-radius: var(--r-full);
font-size: .9rem;
font-weight: 500;
color: rgba(255,255,255,.8);
transition: color var(--t-fast), background var(--t-fast);
}
.nav__link:hover { color: var(--white); background: rgba(255,255,255,.08); }
.nav__link--cta {
background: var(--grad-cta);
color: var(--white) !important;
padding: .5rem 1.2rem;
font-weight: 600;
}
.nav__link--cta:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0,188,212,.4);
background: var(--grad-cta);
}
/* Hamburger */
.nav__toggle {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: .5rem;
}
.nav__toggle-bar {
display: block;
width: 24px;
height: 2px;
background: var(--white);
border-radius: 2px;
transition: transform var(--t-base), opacity var(--t-base);
}
/* ── HERO ───────────────────────────────────────────────────── */
.hero {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
overflow: hidden;
padding-top: var(--nav-h);
background: var(--grad-hero);
}
.hero__mesh-bg {
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 20% 50%, rgba(0,188,212,.12) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(21,101,192,.18) 0%, transparent 45%),
radial-gradient(circle at 60% 80%, rgba(245,124,0,.07) 0%, transparent 40%);
/* Mesh dot pattern */
background-image:
radial-gradient(circle at 20% 50%, rgba(0,188,212,.12) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(21,101,192,.18) 0%, transparent 45%),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Ccircle cx='30' cy='30' r='1' fill='rgba(255,255,255,0.06)'/%3E%3C/svg%3E");
background-repeat: no-repeat, no-repeat, repeat;
animation: meshPulse 8s ease-in-out infinite alternate;
}
@keyframes meshPulse {
from { opacity: .7; }
to { opacity: 1; }
}
.hero__content {
position: relative;
z-index: 1;
max-width: 700px;
padding-block: 4rem;
}
.hero__eyebrow {
font-size: .8rem;
font-weight: 600;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--teal);
margin-bottom: 1rem;
}
.hero__heading {
font-family: var(--font-heading);
font-size: clamp(2.4rem, 6vw, 4rem);
font-weight: 700;
line-height: 1.1;
margin-bottom: 1.25rem;
}
.hero__sub {
font-size: clamp(1rem, 2vw, 1.2rem);
color: rgba(255,255,255,.78);
max-width: 560px;
margin-bottom: 2rem;
}
.hero__actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.hero__scroll-hint {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: .4rem;
}
.hero__scroll-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--teal);
animation: scrollBounce 1.8s ease-in-out infinite;
opacity: .6;
}
@keyframes scrollBounce {
0%, 100% { transform: translateY(0); opacity: .6; }
50% { transform: translateY(8px); opacity: 1; }
}
/* ── STATS BAR ──────────────────────────────────────────────── */
.stats-bar {
background: rgba(255,255,255,.04);
border-top: 1px solid rgba(255,255,255,.07);
border-bottom: 1px solid rgba(255,255,255,.07);
padding-block: 2.5rem;
}
.stats-bar__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
text-align: center;
}
.stat__value {
font-family: var(--font-heading);
font-size: 2.5rem;
font-weight: 700;
color: var(--teal);
}
.stat__unit {
font-family: var(--font-heading);
font-size: 1.2rem;
font-weight: 600;
color: var(--teal);
}
.stat__label {
display: block;
font-size: .82rem;
color: var(--gray-300);
text-transform: uppercase;
letter-spacing: .08em;
margin-top: .25rem;
}
/* ── CARDS ──────────────────────────────────────────────────── */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
}
.card {
background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.08);
border-radius: var(--r-lg);
padding: 2rem 1.75rem;
transition: transform var(--t-base), box-shadow var(--t-base), border-color var(--t-base);
}
.card--hover:hover {
transform: translateY(-5px);
box-shadow: 0 16px 40px rgba(0,0,0,.35);
border-color: rgba(0,188,212,.3);
}
.card__icon {
width: 48px;
height: 48px;
margin-bottom: 1.25rem;
color: var(--teal);
}
.card__icon svg { width: 100%; height: 100%; }
.card__title {
font-family: var(--font-heading);
font-size: 1.1rem;
font-weight: 600;
margin-bottom: .6rem;
}
.card__body {
color: var(--gray-300);
font-size: .93rem;
line-height: 1.6;
margin-bottom: 1.25rem;
}
.card__link {
font-weight: 600;
font-size: .88rem;
color: var(--teal);
letter-spacing: .03em;
}
.card__link:hover { color: var(--orange-lt); }
/* ── WHY ALWISP ─────────────────────────────────────────────── */
.why__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
}
.why__text .section__heading { text-align: left; }
.why__text p {
color: var(--gray-300);
margin-block: 1rem 1.5rem;
max-width: 480px;
}
.why__list {
display: flex;
flex-direction: column;
gap: .6rem;
}
.why__list li {
display: flex;
align-items: center;
gap: .75rem;
font-size: .95rem;
color: rgba(255,255,255,.85);
}
.why__check {
color: var(--teal);
font-weight: 700;
font-size: 1rem;
flex-shrink: 0;
}
/* Mesh SVG diagram */
.why__visual { display: flex; justify-content: center; }
.mesh-diagram { width: 100%; max-width: 380px; }
.mesh-svg { width: 100%; height: auto; }
.mesh-node {
filter: drop-shadow(0 0 6px currentColor);
}
.mesh-node--tower {
filter: drop-shadow(0 0 10px rgba(245,124,0,.8));
animation: nodePulse 2.5s ease-in-out infinite;
}
@keyframes nodePulse {
0%, 100% { r: 12; opacity: .9; }
50% { r: 14; opacity: 1; }
}
/* ── CTA BAND ───────────────────────────────────────────────── */
.cta-band {
background: linear-gradient(135deg, rgba(21,101,192,.25), rgba(0,188,212,.15));
border-top: 1px solid rgba(0,188,212,.2);
border-bottom: 1px solid rgba(0,188,212,.2);
}
.cta-band__content { text-align: center; }
.cta-band__heading {
font-family: var(--font-heading);
font-size: clamp(1.5rem, 3vw, 2.2rem);
font-weight: 700;
margin-bottom: .6rem;
}
.cta-band__sub { color: var(--gray-300); margin-bottom: 1.75rem; }
.cta-band__form {
display: flex;
gap: .75rem;
justify-content: center;
flex-wrap: wrap;
max-width: 560px;
margin-inline: auto;
}
.cta-band__input {
flex: 1;
min-width: 240px;
padding: .75rem 1.25rem;
border-radius: var(--r-full);
border: 1px solid rgba(255,255,255,.2);
background: rgba(255,255,255,.07);
color: var(--white);
font-size: .95rem;
outline: none;
transition: border-color var(--t-fast), background var(--t-fast);
}
.cta-band__input::placeholder { color: var(--gray-300); }
.cta-band__input:focus {
border-color: var(--teal);
background: rgba(255,255,255,.12);
}
/* ── PAGE HERO ──────────────────────────────────────────────── */
.page-hero {
padding-top: calc(var(--nav-h) + 3rem);
padding-bottom: 3rem;
background: linear-gradient(135deg, #0d1b3e 0%, #132145 100%);
border-bottom: 1px solid rgba(255,255,255,.06);
text-align: center;
}
/* ── CONTACT ────────────────────────────────────────────────── */
.contact__grid {
display: grid;
grid-template-columns: 280px 1fr;
gap: 4rem;
align-items: start;
}
.contact__heading {
font-family: var(--font-heading);
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 1.25rem;
}
.contact__details {
display: flex;
flex-direction: column;
gap: 1rem;
color: var(--gray-300);
font-size: .95rem;
line-height: 1.7;
}
.contact__details a { color: var(--teal); }
/* ── FORM ───────────────────────────────────────────────────── */
.form { display: flex; flex-direction: column; gap: 1.25rem; }
.form__row { display: flex; gap: 1rem; }
.form__row--2 > * { flex: 1; }
.form__group { display: flex; flex-direction: column; gap: .4rem; }
.form__label {
font-size: .85rem;
font-weight: 500;
color: var(--gray-300);
}
.form__label span { color: var(--teal); }
.form__input {
padding: .7rem 1rem;
border-radius: var(--r-sm);
border: 1px solid rgba(255,255,255,.15);
background: rgba(255,255,255,.06);
color: var(--white);
font-size: .95rem;
font-family: var(--font-body);
outline: none;
transition: border-color var(--t-fast);
}
.form__input:focus { border-color: var(--teal); background: rgba(255,255,255,.1); }
.form__textarea { resize: vertical; min-height: 130px; }
select.form__input option { background: var(--navy); }
/* ── ALERTS ─────────────────────────────────────────────────── */
.alert {
padding: 1rem 1.25rem;
border-radius: var(--r-sm);
font-size: .93rem;
margin-bottom: 1.25rem;
}
.alert--success { background: rgba(0,188,212,.15); border: 1px solid rgba(0,188,212,.4); color: #b2ebf2; }
.alert--error { background: rgba(229,57,53,.15); border: 1px solid rgba(229,57,53,.4); color: #ffcdd2; }
.alert ul { list-style: disc; padding-left: 1.25rem; }
/* ── FOOTER ─────────────────────────────────────────────────── */
.site-footer {
background: #080f1f;
border-top: 1px solid rgba(255,255,255,.07);
padding-top: 3.5rem;
}
.footer__grid {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr 1.2fr;
gap: 2.5rem;
padding-bottom: 3rem;
}
.footer__tagline {
color: var(--gray-300);
font-size: .9rem;
line-height: 1.7;
margin-top: .75rem;
}
.footer__heading {
font-family: var(--font-heading);
font-size: .8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .1em;
color: var(--teal);
margin-bottom: 1rem;
}
.footer__links ul { display: flex; flex-direction: column; gap: .5rem; }
.footer__links a {
color: var(--gray-300);
font-size: .9rem;
}
.footer__links a:hover { color: var(--white); }
.footer__contact p {
color: var(--gray-300);
font-size: .9rem;
margin-bottom: .5rem;
}
.footer__contact a { color: var(--gray-300); }
.footer__contact a:hover { color: var(--teal); }
.footer__social {
display: flex;
gap: .5rem;
margin-top: 1rem;
}
.footer__social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: var(--r-sm);
background: rgba(255,255,255,.07);
font-size: .72rem;
font-weight: 700;
color: var(--gray-300);
transition: background var(--t-fast), color var(--t-fast);
}
.footer__social-link:hover { background: var(--teal); color: var(--white); }
.footer__bottom {
padding-block: 1.25rem;
border-top: 1px solid rgba(255,255,255,.06);
text-align: center;
color: var(--gray-500);
font-size: .82rem;
}
/* ── RESPONSIVE ─────────────────────────────────────────────── */
@media (max-width: 1024px) {
.why__grid { grid-template-columns: 1fr; gap: 2.5rem; }
.why__visual { display: none; }
.footer__grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 768px) {
:root { --section-py: 3.5rem; }
/* Mobile nav */
.nav__toggle { display: flex; }
.nav__menu {
position: fixed;
top: var(--nav-h);
left: 0; right: 0;
background: rgba(13,27,62,.98);
flex-direction: column;
align-items: stretch;
padding: 1rem 1.5rem 2rem;
gap: .25rem;
border-bottom: 1px solid rgba(255,255,255,.1);
transform: translateY(-110%);
opacity: 0;
transition: transform var(--t-base), opacity var(--t-base);
pointer-events: none;
}
.nav__menu.is-open {
transform: translateY(0);
opacity: 1;
pointer-events: auto;
}
.nav__link { padding: .75rem 1rem; border-radius: var(--r-sm); }
.nav__link--cta { text-align: center; margin-top: .5rem; }
.stats-bar__grid { grid-template-columns: repeat(2, 1fr); gap: 1.5rem; }
.contact__grid { grid-template-columns: 1fr; gap: 2rem; }
.form__row--2 { flex-direction: column; }
.footer__grid { grid-template-columns: 1fr; gap: 2rem; }
}
@media (max-width: 480px) {
.hero__actions { flex-direction: column; }
.hero__actions .btn { text-align: center; justify-content: center; }
.stats-bar__grid { grid-template-columns: 1fr 1fr; }
.cta-band__form { flex-direction: column; }
.cta-band__input { min-width: 100%; }
}

53
www/includes/footer.php Normal file
View File

@@ -0,0 +1,53 @@
</main>
<!-- ===================== FOOTER ===================== -->
<footer class="site-footer">
<div class="container footer__grid">
<div class="footer__brand">
<a href="/" class="nav__logo" aria-label="ALWISP Home">
<span class="nav__logo-text">AL<span class="accent">WISP</span></span>
</a>
<p class="footer__tagline">Connecting Alabama,<br>one node at a time.</p>
</div>
<div class="footer__links">
<h4 class="footer__heading">Services</h4>
<ul>
<li><a href="/services#residential">Residential Internet</a></li>
<li><a href="/services#business">Business Internet</a></li>
<li><a href="/services#infrastructure">Network Infrastructure</a></li>
<li><a href="/services#managed">Managed Networking</a></li>
</ul>
</div>
<div class="footer__links">
<h4 class="footer__heading">Company</h4>
<ul>
<li><a href="/about">About Us</a></li>
<li><a href="/coverage">Coverage Map</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer__contact">
<h4 class="footer__heading">Contact</h4>
<p>📞 <a href="tel:+1-000-000-0000">(000) 000-0000</a></p>
<p> <a href="mailto:info@alwisp.net">info@alwisp.net</a></p>
<div class="footer__social">
<!-- Social icons add links when ready -->
<a href="#" aria-label="Facebook" class="footer__social-link">FB</a>
<a href="#" aria-label="Twitter/X" class="footer__social-link">X</a>
</div>
</div>
</div>
<div class="footer__bottom container">
<p>&copy; <?= date('Y') ?> Alabama WISP Mesh Network Solutions. All rights reserved.</p>
</div>
</footer>
<script src="/js/main.js" defer></script>
</body>
</html>

45
www/includes/header.php Normal file
View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Alabama WISP Mesh Network Solutions. High-speed wireless internet and enterprise networking for rural and underserved communities.">
<title>ALWISP Mesh Network Solutions</title>
<!-- Preconnect for performance -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Google Fonts: Inter (body) + Space Grotesk (headings) -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
</head>
<body>
<!-- ===================== NAV ===================== -->
<header class="site-header" id="site-header">
<nav class="nav container">
<a href="/" class="nav__logo" aria-label="ALWISP Home">
<img src="/assets/logo-condensed.png" alt="ALWISP Logo" class="nav__logo-img" onerror="this.style.display='none'">
<span class="nav__logo-text">AL<span class="accent">WISP</span></span>
</a>
<button class="nav__toggle" id="nav-toggle" aria-label="Open menu" aria-expanded="false" aria-controls="nav-menu">
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
</button>
<ul class="nav__menu" id="nav-menu" role="menubar">
<li role="none"><a href="/" class="nav__link" role="menuitem">Home</a></li>
<li role="none"><a href="/services" class="nav__link" role="menuitem">Services</a></li>
<li role="none"><a href="/coverage" class="nav__link" role="menuitem">Coverage</a></li>
<li role="none"><a href="/about" class="nav__link" role="menuitem">About</a></li>
<li role="none"><a href="/contact" class="nav__link nav__link--cta" role="menuitem">Get Connected</a></li>
</ul>
</nav>
</header>
<main id="main-content">

20
www/index.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
// Simple front controller — expand routing here later
$path = trim($_GET['path'] ?? '', '/');
// Map paths to page includes
$pages = [
'' => 'pages/home.php',
'services' => 'pages/services.php',
'coverage' => 'pages/coverage.php',
'about' => 'pages/about.php',
'contact' => 'pages/contact.php',
];
$page = $pages[$path] ?? 'pages/404.php';
$pageFile = __DIR__ . '/' . $page;
include __DIR__ . '/includes/header.php';
include file_exists($pageFile) ? $pageFile : __DIR__ . '/pages/404.php';
include __DIR__ . '/includes/footer.php';
?>

114
www/js/main.js Normal file
View File

@@ -0,0 +1,114 @@
/* ============================================================
ALWISP main.js
============================================================ */
'use strict';
// ── NAV: scroll shadow + mobile toggle ──────────────────────
(function () {
const header = document.getElementById('site-header');
const toggle = document.getElementById('nav-toggle');
const menu = document.getElementById('nav-menu');
const navLinks = menu ? menu.querySelectorAll('.nav__link') : [];
// Scroll shadow
if (header) {
const onScroll = () => header.classList.toggle('scrolled', window.scrollY > 10);
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
}
// Mobile toggle
if (toggle && menu) {
toggle.addEventListener('click', () => {
const isOpen = menu.classList.toggle('is-open');
toggle.setAttribute('aria-expanded', isOpen);
});
// Close on nav link click
navLinks.forEach(link => {
link.addEventListener('click', () => {
menu.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
});
});
// Close on outside click
document.addEventListener('click', e => {
if (!header.contains(e.target)) {
menu.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
}
});
}
// Active nav link
const currentPath = window.location.pathname.replace(/\/$/, '') || '/';
navLinks.forEach(link => {
const href = link.getAttribute('href').replace(/\/$/, '') || '/';
if (href === currentPath) link.classList.add('nav__link--active');
});
}());
// ── COUNTER ANIMATION (stats bar) ───────────────────────────
(function () {
const counters = document.querySelectorAll('.stat__value[data-count]');
if (!counters.length) return;
const easeOut = t => 1 - Math.pow(1 - t, 3);
function animateCounter(el) {
const target = parseInt(el.dataset.count, 10);
const duration = 1400;
const start = performance.now();
function step(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
el.textContent = Math.floor(easeOut(progress) * target);
if (progress < 1) requestAnimationFrame(step);
else el.textContent = target;
}
requestAnimationFrame(step);
}
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
animateCounter(entry.target);
observer.unobserve(entry.target);
}
});
}, { threshold: 0.4 });
counters.forEach(c => observer.observe(c));
}());
// ── SCROLL REVEAL (cards, sections) ─────────────────────────
(function () {
const revealEls = document.querySelectorAll(
'.card, .stat, .why__text, .section__header, .cta-band__content'
);
if (!revealEls.length || !('IntersectionObserver' in window)) return;
revealEls.forEach((el, i) => {
el.style.opacity = '0';
el.style.transform = 'translateY(22px)';
el.style.transition = `opacity 0.55s ease ${i * 0.07}s, transform 0.55s ease ${i * 0.07}s`;
});
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
observer.unobserve(entry.target);
}
});
}, { threshold: 0.12 });
revealEls.forEach(el => observer.observe(el));
}());

9
www/pages/404.php Normal file
View File

@@ -0,0 +1,9 @@
<?php http_response_code(404); ?>
<section class="section page-hero" style="text-align:center; min-height:60vh; display:flex; align-items:center;">
<div class="container">
<p class="hero__eyebrow">Error 404</p>
<h1 class="section__heading">Page Not Found</h1>
<p class="section__sub">The page you're looking for doesn't exist or has moved.</p>
<a href="/" class="btn btn--primary" style="margin-top:2rem">Back to Home</a>
</div>
</section>

14
www/pages/about.php Normal file
View File

@@ -0,0 +1,14 @@
<!-- ABOUT PAGE placeholder -->
<section class="section page-hero">
<div class="container">
<span class="section__eyebrow">Who We Are</span>
<h1 class="section__heading">About ALWISP</h1>
<p class="section__sub">Alabama's locally-owned wireless internet and mesh networking company.</p>
</div>
</section>
<section class="section">
<div class="container" style="max-width:800px">
<div class="placeholder-block">Company story, team bios, and mission statement will go here.</div>
</div>
</section>

96
www/pages/contact.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
$success = false;
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim(htmlspecialchars($_POST['name'] ?? '', ENT_QUOTES));
$email = trim(htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES));
$phone = trim(htmlspecialchars($_POST['phone'] ?? '', ENT_QUOTES));
$subject = trim(htmlspecialchars($_POST['subject'] ?? '', ENT_QUOTES));
$message = trim(htmlspecialchars($_POST['message'] ?? '', ENT_QUOTES));
if (!$name) $errors[] = 'Name is required.';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'A valid email is required.';
if (!$message) $errors[] = 'Message is required.';
if (empty($errors)) {
// TODO: swap for DB insert + email once credentials are configured
$success = true;
}
}
?>
<section class="section page-hero">
<div class="container">
<span class="section__eyebrow">Reach Out</span>
<h1 class="section__heading">Contact Us</h1>
<p class="section__sub">Questions about service, coverage, or your account? We're here to help.</p>
</div>
</section>
<section class="section">
<div class="container contact__grid">
<div class="contact__info">
<h2 class="contact__heading">Get In Touch</h2>
<ul class="contact__details">
<li>📞 <a href="tel:+10000000000">(000) 000-0000</a></li>
<li>✉ <a href="mailto:info@alwisp.net">info@alwisp.net</a></li>
<li>🕐 MonFri 8am6pm CST<br>Emergency support 24/7</li>
</ul>
</div>
<div class="contact__form-wrap">
<?php if ($success): ?>
<div class="alert alert--success" role="alert">
Thanks! We'll be in touch within one business day.
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="alert alert--error" role="alert">
<ul><?php foreach ($errors as $e) echo "<li>" . $e . "</li>"; ?></ul>
</div>
<?php endif; ?>
<form method="post" action="/contact" class="form" novalidate>
<div class="form__row form__row--2">
<div class="form__group">
<label for="name" class="form__label">Name <span aria-hidden="true">*</span></label>
<input type="text" id="name" name="name" class="form__input" required
value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
</div>
<div class="form__group">
<label for="email" class="form__label">Email <span aria-hidden="true">*</span></label>
<input type="email" id="email" name="email" class="form__input" required
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>">
</div>
</div>
<div class="form__row form__row--2">
<div class="form__group">
<label for="phone" class="form__label">Phone</label>
<input type="tel" id="phone" name="phone" class="form__input"
value="<?= htmlspecialchars($_POST['phone'] ?? '') ?>">
</div>
<div class="form__group">
<label for="subject" class="form__label">Subject</label>
<select id="subject" name="subject" class="form__input">
<option value="">Select a topic…</option>
<option value="new-service">New Service Inquiry</option>
<option value="support">Technical Support</option>
<option value="billing">Billing Question</option>
<option value="coverage">Coverage Question</option>
<option value="other">Other</option>
</select>
</div>
</div>
<div class="form__group">
<label for="message" class="form__label">Message <span aria-hidden="true">*</span></label>
<textarea id="message" name="message" class="form__input form__textarea" rows="5" required><?= htmlspecialchars($_POST['message'] ?? '') ?></textarea>
</div>
<button type="submit" class="btn btn--primary">Send Message</button>
</form>
</div>
</div>
</section>

17
www/pages/coverage.php Normal file
View File

@@ -0,0 +1,17 @@
<!-- COVERAGE PAGE placeholder -->
<section class="section page-hero">
<div class="container">
<span class="section__eyebrow">Service Area</span>
<h1 class="section__heading">Coverage Map</h1>
<p class="section__sub">See where ALWISP service is available.</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="placeholder-block" style="min-height:420px; display:flex; align-items:center; justify-content:center;">
Interactive coverage map will be embedded here.<br>
(Leaflet.js / Google Maps integration roadmap item)
</div>
</div>
</section>

170
www/pages/home.php Normal file
View File

@@ -0,0 +1,170 @@
<!-- ====================================================
HOME PAGE
===================================================== -->
<!-- HERO -->
<section class="hero" aria-labelledby="hero-heading">
<div class="hero__mesh-bg" aria-hidden="true"></div>
<div class="container hero__content">
<p class="hero__eyebrow">Alabama's Wireless Network Provider</p>
<h1 id="hero-heading" class="hero__heading">
Fast, Reliable Internet<br>
<span class="gradient-text">Built for Alabama</span>
</h1>
<p class="hero__sub">
Enterprise-grade mesh networking that reaches where fiber can't.
Residential plans, business solutions, and infrastructure buildouts across Alabama.
</p>
<div class="hero__actions">
<a href="/coverage" class="btn btn--primary">Check My Coverage</a>
<a href="/services" class="btn btn--ghost">View Plans</a>
</div>
</div>
<div class="hero__scroll-hint" aria-hidden="true">
<span class="hero__scroll-dot"></span>
</div>
</section>
<!-- STATS BAR -->
<section class="stats-bar" aria-label="Quick statistics">
<div class="container stats-bar__grid">
<div class="stat">
<span class="stat__value" data-count="500">0</span><span class="stat__unit">+</span>
<span class="stat__label">Active Subscribers</span>
</div>
<div class="stat">
<span class="stat__value" data-count="15">0</span><span class="stat__unit"> Counties</span>
<span class="stat__label">Coverage Area</span>
</div>
<div class="stat">
<span class="stat__value" data-count="99">0</span><span class="stat__unit">%</span>
<span class="stat__label">Uptime SLA</span>
</div>
<div class="stat">
<span class="stat__value" data-count="1">0</span><span class="stat__unit">Gbps</span>
<span class="stat__label">Backbone Capacity</span>
</div>
</div>
</section>
<!-- SERVICES PREVIEW -->
<section class="section services-preview" aria-labelledby="services-heading">
<div class="container">
<div class="section__header">
<span class="section__eyebrow">What We Offer</span>
<h2 id="services-heading" class="section__heading">Connectivity Solutions</h2>
<p class="section__sub">From home internet to full network infrastructure buildouts.</p>
</div>
<div class="cards">
<div class="card card--hover">
<div class="card__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
</div>
<h3 class="card__title">Residential Internet</h3>
<p class="card__body">High-speed wireless internet for homes and small properties. No contracts, transparent pricing.</p>
<a href="/services#residential" class="card__link">See Plans </a>
</div>
<div class="card card--hover">
<div class="card__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/></svg>
</div>
<h3 class="card__title">Business Internet</h3>
<p class="card__body">Dedicated bandwidth, static IPs, and priority support for businesses of any size.</p>
<a href="/services#business" class="card__link">See Plans </a>
</div>
<div class="card card--hover">
<div class="card__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><circle cx="12" cy="5" r="1"/><circle cx="19" cy="9" r="1"/><circle cx="19" cy="15" r="1"/><circle cx="12" cy="19" r="1"/><circle cx="5" cy="15" r="1"/><circle cx="5" cy="9" r="1"/><path d="M12 7v3m5.2.8-2.5 1.5m2.5 4.7-2.5-1.5M12 17v-3m-5.2-.8 2.5-1.5m-2.5-4.7 2.5 1.5"/></svg>
</div>
<h3 class="card__title">Mesh Infrastructure</h3>
<p class="card__body">Full network design, tower buildouts, and mesh deployments for communities and enterprises.</p>
<a href="/services#infrastructure" class="card__link">Learn More </a>
</div>
<div class="card card--hover">
<div class="card__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h3 class="card__title">Managed Networking</h3>
<p class="card__body">Ongoing monitoring, maintenance, and support so your network runs itself.</p>
<a href="/services#managed" class="card__link">Learn More </a>
</div>
</div>
</div>
</section>
<!-- WHY ALWISP -->
<section class="section section--alt why" aria-labelledby="why-heading">
<div class="container why__grid">
<div class="why__text">
<span class="section__eyebrow">Why ALWISP</span>
<h2 id="why-heading" class="section__heading">Local Team.<br>Enterprise Tech.</h2>
<p>We're an Alabama company solving Alabama's connectivity gap. We deploy the same equipment used by major carriers without the corporate red tape or out-of-state call centers.</p>
<ul class="why__list">
<li><span class="why__check" aria-hidden="true"></span> Local 24/7 technical support</li>
<li><span class="why__check" aria-hidden="true"></span> No data caps on most plans</li>
<li><span class="why__check" aria-hidden="true"></span> Month-to-month options available</li>
<li><span class="why__check" aria-hidden="true"></span> Self-healing mesh redundancy</li>
<li><span class="why__check" aria-hidden="true"></span> Transparent pricing no hidden fees</li>
</ul>
<a href="/about" class="btn btn--primary" style="margin-top:1.5rem">About Us</a>
</div>
<div class="why__visual" aria-hidden="true">
<div class="mesh-diagram">
<!-- SVG mesh nodes decorative -->
<svg viewBox="0 0 400 340" xmlns="http://www.w3.org/2000/svg" class="mesh-svg">
<defs>
<linearGradient id="lineGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00bcd4" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#1565c0" stop-opacity="0.3"/>
</linearGradient>
</defs>
<!-- Edges -->
<g stroke="url(#lineGrad)" stroke-width="1.5" fill="none">
<line x1="200" y1="60" x2="320" y2="140"/>
<line x1="200" y1="60" x2="80" y2="140"/>
<line x1="320" y1="140" x2="80" y2="140"/>
<line x1="320" y1="140" x2="280" y2="280"/>
<line x1="80" y1="140" x2="120" y2="280"/>
<line x1="280" y1="280" x2="120" y2="280"/>
<line x1="200" y1="60" x2="200" y2="200"/>
<line x1="200" y1="200" x2="320" y2="140"/>
<line x1="200" y1="200" x2="80" y2="140"/>
<line x1="200" y1="200" x2="280" y2="280"/>
<line x1="200" y1="200" x2="120" y2="280"/>
</g>
<!-- Nodes -->
<g>
<circle cx="200" cy="60" r="12" fill="#f57c00" opacity="0.9" class="mesh-node mesh-node--tower"/>
<circle cx="320" cy="140" r="9" fill="#00bcd4" opacity="0.85" class="mesh-node"/>
<circle cx="80" cy="140" r="9" fill="#00bcd4" opacity="0.85" class="mesh-node"/>
<circle cx="200" cy="200" r="10" fill="#1565c0" opacity="0.9" class="mesh-node"/>
<circle cx="280" cy="280" r="8" fill="#00acc1" opacity="0.8" class="mesh-node"/>
<circle cx="120" cy="280" r="8" fill="#00acc1" opacity="0.8" class="mesh-node"/>
</g>
</svg>
</div>
</div>
</div>
</section>
<!-- COVERAGE CTA -->
<section class="section cta-band" aria-labelledby="cta-heading">
<div class="container cta-band__content">
<h2 id="cta-heading" class="cta-band__heading">Are you in our coverage area?</h2>
<p class="cta-band__sub">Enter your address to see available plans in your area.</p>
<form class="cta-band__form" action="/coverage" method="get" role="search" aria-label="Coverage check">
<input type="text" name="address" placeholder="Enter your address…" class="cta-band__input" aria-label="Street address" required>
<button type="submit" class="btn btn--primary">Check Now</button>
</form>
</div>
</section>

45
www/pages/services.php Normal file
View File

@@ -0,0 +1,45 @@
<!-- SERVICES PAGE placeholder -->
<section class="section page-hero page-hero--services">
<div class="container">
<span class="section__eyebrow">What We Offer</span>
<h1 class="section__heading">Our Services</h1>
<p class="section__sub">Flexible plans for every need from a farmhouse to a full enterprise campus.</p>
</div>
</section>
<section class="section" id="residential">
<div class="container">
<div class="section__header">
<h2 class="section__heading">Residential Plans</h2>
<p class="section__sub">Content coming soon plan tiers will be displayed here.</p>
</div>
<div class="placeholder-block">Plan cards will go here.</div>
</div>
</section>
<section class="section section--alt" id="business">
<div class="container">
<div class="section__header">
<h2 class="section__heading">Business Plans</h2>
</div>
<div class="placeholder-block">Business plan cards will go here.</div>
</div>
</section>
<section class="section" id="infrastructure">
<div class="container">
<div class="section__header">
<h2 class="section__heading">Network Infrastructure</h2>
</div>
<div class="placeholder-block">Infrastructure service details will go here.</div>
</div>
</section>
<section class="section section--alt" id="managed">
<div class="container">
<div class="section__header">
<h2 class="section__heading">Managed Networking</h2>
</div>
<div class="placeholder-block">Managed service details will go here.</div>
</div>
</section>