diff --git a/.env.example b/.env.example index f58040a..6484b3c 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,11 @@ -# Secret key for JWT +# Secret key for JWT (generate with: openssl rand -base64 32) SECRET_KEY= -# Base URL -BASE_URL=http://localhost:8000 +# Dev server port (8001 avoids conflict with other projects on 8000) +APP_PORT=8001 + +# Base URL (must match APP_PORT for local dev) +BASE_URL=http://127.0.0.1:8001 # Database connection mode (0 = direct, 1 = pooled) USE_POOL=0 @@ -14,8 +17,9 @@ DB_SSLMODE=prefer # Direct connection settings (when USE_POOL=0) DB_USER=postgres DB_PASSWORD=postgres -DB_PORT=5432 -DB_NAME= +# Use 5433 if another project already uses 5432 +DB_PORT=5433 +DB_NAME=calliflowr # Pooled connection settings (when USE_POOL=1) # DB_APPUSER= @@ -37,4 +41,7 @@ DOMAIN= # Resend RESEND_API_KEY= -EMAIL_FROM= \ No newline at end of file +EMAIL_FROM= + +# Website lead notifications (defaults to hello@calliflowr.com) +# LEADS_NOTIFY_EMAIL= \ No newline at end of file diff --git a/dev.py b/dev.py new file mode 100644 index 0000000..3d3978f --- /dev/null +++ b/dev.py @@ -0,0 +1,14 @@ +"""Start the local dev server (reads APP_PORT from .env).""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +if __name__ == "__main__": + import uvicorn + + port = int(os.getenv("APP_PORT", "8001")) + host = os.getenv("APP_HOST", "127.0.0.1") + uvicorn.run("main:app", host=host, port=port, reload=True) diff --git a/main.py b/main.py index d2f7935..15d8bdd 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,8 @@ import logging +import os from contextlib import asynccontextmanager from dotenv import load_dotenv -from fastapi import FastAPI, Request, Depends, status +from fastapi import FastAPI, Request, status from fastapi.responses import RedirectResponse, Response from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -16,9 +17,9 @@ static_pages, invitation, ) +from routers.website import home, demo, leads from utils.core.dependencies import ( get_user_from_request, - require_unauthenticated_client, ) from utils.core.auth import refresh_token_is_persistent, set_auth_cookies from utils.core.rate_limit import get_trusted_proxy_hosts @@ -109,6 +110,9 @@ async def csrf_middleware(request: Request, call_next): # --- Include Routers --- +app.include_router(home.router) +app.include_router(demo.router) +app.include_router(leads.router) app.include_router(account.router) app.include_router(dashboard.router) app.include_router(invitation.router) @@ -389,17 +393,8 @@ async def general_exception_handler(request: Request, exc: Exception): ) -# --- Home Page --- - - -@app.get("/") -async def read_home( - request: Request, _: None = Depends(require_unauthenticated_client) -): - return templates.TemplateResponse(request, "index.html", {"user": None}) - - if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + port = int(os.getenv("APP_PORT", "8001")) + uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/routers/core/static_pages.py b/routers/core/static_pages.py index ca859fb..5b78e54 100644 --- a/routers/core/static_pages.py +++ b/routers/core/static_pages.py @@ -3,6 +3,7 @@ from fastapi.templating import Jinja2Templates from utils.core.dependencies import get_optional_user from utils.core.models import User +from utils.website.content import website_home_context router = APIRouter(tags=["static_pages"]) templates = Jinja2Templates(directory="templates") @@ -10,8 +11,19 @@ # Define valid static pages to prevent arbitrary template access VALID_PAGES = { "about": "static_pages/about.html", + "blog": "static_pages/blog.html", + "customers": "static_pages/customers.html", "privacy-policy": "static_pages/privacy_policy.html", - "terms-of-service": "static_pages/terms_of_service.html", + "terms-and-conditions": "static_pages/terms_and_conditions.html", + "website-fulfillment-policy": "static_pages/website_fulfillment_policy.html", +} + +WEBSITE_THEMED_PAGES = { + "about", + "blog", + "privacy-policy", + "terms-and-conditions", + "website-fulfillment-policy", } @@ -36,4 +48,8 @@ async def read_static_page( if page_name not in VALID_PAGES: raise HTTPException(status_code=404, detail="Page not found") - return templates.TemplateResponse(request, VALID_PAGES[page_name], {"user": user}) + context: dict = {"user": user} + if page_name in WEBSITE_THEMED_PAGES: + context.update(website_home_context()) + + return templates.TemplateResponse(request, VALID_PAGES[page_name], context) diff --git a/routers/website/__init__.py b/routers/website/__init__.py new file mode 100644 index 0000000..0e9a8ba --- /dev/null +++ b/routers/website/__init__.py @@ -0,0 +1,3 @@ +from routers.website import demo, home, leads + +__all__ = ["demo", "home", "leads"] diff --git a/routers/website/demo.py b/routers/website/demo.py new file mode 100644 index 0000000..fc278df --- /dev/null +++ b/routers/website/demo.py @@ -0,0 +1,112 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import RedirectResponse +from fastapi.templating import Jinja2Templates + +from utils.core.dependencies import get_optional_user +from utils.core.htmx import set_flash_cookie +from utils.core.models import User +from exceptions.http_exceptions import RateLimitError +from utils.core.rate_limit import check_website_lead_ip_rate_limit +from utils.website.content import website_demo_context +from utils.website.forms import BookDemoSubmission +from utils.website.notifications import notify_book_demo + +router = APIRouter(tags=["website"]) +templates = Jinja2Templates(directory="templates") + + +def _demo_template_context( + request: Request, + user: Optional[User], + *, + form_values: dict | None = None, + form_errors: dict | None = None, + form_error: str | None = None, +) -> dict: + return { + "request": request, + "user": user, + "form_values": form_values or {}, + "form_errors": form_errors or {}, + "form_error": form_error, + **website_demo_context(), + } + + +@router.get("/demo", name="read_demo") +async def read_demo( + request: Request, + user: Optional[User] = Depends(get_optional_user), +): + """Public book-a-demo page.""" + return templates.TemplateResponse( + request, + "website/demo.html", + _demo_template_context(request, user), + ) + + +@router.post("/demo", name="submit_book_demo") +async def submit_book_demo( + request: Request, + user: Optional[User] = Depends(get_optional_user), + work_email: str = Form(""), + first_name: str = Form(""), + last_name: str = Form(""), + company: str = Form(""), + trade: str = Form(""), + phone: str = Form(""), + technicians: str = Form(""), + phone_answerer: str = Form(""), +): + form_values = { + "work_email": work_email, + "first_name": first_name, + "last_name": last_name, + "company": company, + "trade": trade, + "phone": phone, + "technicians": technicians, + "phone_answerer": phone_answerer, + } + + try: + check_website_lead_ip_rate_limit(request) + except RateLimitError: + return templates.TemplateResponse( + request, + "website/demo.html", + _demo_template_context( + request, + user, + form_values=form_values, + form_error="Too many requests. Please try again in a little while.", + ), + status_code=429, + ) + + submission, form_errors = BookDemoSubmission.from_form(**form_values) + if submission is None: + return templates.TemplateResponse( + request, + "website/demo.html", + _demo_template_context( + request, + user, + form_values=form_values, + form_errors=form_errors, + ), + status_code=422, + ) + + notify_book_demo(submission) + + response = RedirectResponse(url=str(request.url_for("read_demo")), status_code=303) + set_flash_cookie( + response, + "Thanks — we'll be in touch shortly to schedule your demo.", + level="success", + ) + return response diff --git a/routers/website/home.py b/routers/website/home.py new file mode 100644 index 0000000..ef8cf87 --- /dev/null +++ b/routers/website/home.py @@ -0,0 +1,25 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, Request +from fastapi.templating import Jinja2Templates + +from utils.core.dependencies import get_optional_user +from utils.core.models import User +from utils.website.content import website_home_context + +router = APIRouter(tags=["website"]) +templates = Jinja2Templates(directory="templates") + + +@router.get("/", name="read_home") +async def read_home( + request: Request, + user: Optional[User] = Depends(get_optional_user), +): + """Public Calliflowr website homepage.""" + context = {"user": user, **website_home_context()} + return templates.TemplateResponse( + request, + "website/index.html", + context, + ) diff --git a/routers/website/leads.py b/routers/website/leads.py new file mode 100644 index 0000000..6b289c6 --- /dev/null +++ b/routers/website/leads.py @@ -0,0 +1,78 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import RedirectResponse + +from utils.core.dependencies import get_optional_user +from utils.core.htmx import set_flash_cookie +from utils.core.models import User +from exceptions.http_exceptions import RateLimitError +from utils.core.rate_limit import check_website_lead_ip_rate_limit +from utils.website.forms import CallMeSubmission +from utils.website.notifications import notify_call_me + +router = APIRouter(tags=["website"]) + + +@router.post("/call-me", name="submit_call_me") +async def submit_call_me( + request: Request, + name: str = Form(""), + business: str = Form(""), + email: str = Form(""), + phone: str = Form(""), + industry: str = Form(""), + crm: str = Form(""), + crm_other: str = Form(""), + user: Optional[User] = Depends(get_optional_user), +): + del user # public endpoint; auth state does not affect submission + + form_values = { + "name": name, + "business": business, + "email": email, + "phone": phone, + "industry": industry, + "crm": crm, + "crm_other": crm_other, + } + + try: + check_website_lead_ip_rate_limit(request) + except RateLimitError: + response = RedirectResponse( + url=str(request.url_for("read_home")) + "#call-me", + status_code=303, + ) + set_flash_cookie( + response, + "Too many requests. Please try again in a little while.", + level="danger", + ) + return response + + submission, form_errors = CallMeSubmission.from_form(**form_values) + if submission is None: + first_error = next( + iter(form_errors.values()), "Please check the form and try again." + ) + response = RedirectResponse( + url=str(request.url_for("read_home")) + "#call-me", + status_code=303, + ) + set_flash_cookie(response, first_error, level="danger") + return response + + notify_call_me(submission) + + response = RedirectResponse( + url=str(request.url_for("read_home")) + "#call-me", + status_code=303, + ) + set_flash_cookie( + response, + "Thanks — we'll have Calli ring you shortly.", + level="success", + ) + return response diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png index 7b8c78d..5eff7af 100644 Binary files a/static/apple-touch-icon.png and b/static/apple-touch-icon.png differ diff --git a/static/css/website/base.css b/static/css/website/base.css new file mode 100644 index 0000000..70d8f4e --- /dev/null +++ b/static/css/website/base.css @@ -0,0 +1,53 @@ +/* + * Website base styles — reset and page shell. + */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-font-smoothing: antialiased; +} + +body.website-body { + margin: 0; + font-family: var(--font-body); + color: var(--color-ink); + background: var(--color-page); +} + +.website-body a { + text-decoration: none; + color: var(--color-link); +} + +.website-body a:hover { + color: var(--color-deep-fir); +} + +.website-site { + width: 100%; + min-height: 100vh; + background: var(--color-page); +} + +.website-display { + font-family: var(--font-display); +} + +@keyframes website-live-pulse { + 0% { + box-shadow: 0 0 0 0 rgba(159, 232, 112, 0.55); + } + + 70% { + box-shadow: 0 0 0 8px rgba(159, 232, 112, 0); + } + + 100% { + box-shadow: 0 0 0 0 rgba(159, 232, 112, 0); + } +} diff --git a/static/css/website/components.css b/static/css/website/components.css new file mode 100644 index 0000000..c39c524 --- /dev/null +++ b/static/css/website/components.css @@ -0,0 +1,536 @@ +/* + * Reusable website UI components. + */ + +/* Live status dot */ +.website-live-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-live-dot); + animation: website-live-pulse 1.8s infinite; + flex: none; +} + +/* Eyebrow / pill badge */ +.website-eyebrow { + display: inline-flex; + align-items: center; + gap: 9px; + padding: 7px 15px; + border-radius: var(--radius-pill); + font-size: 12.5px; + font-weight: 650; + background: var(--color-eyebrow-bg); + border: 1px solid var(--color-eyebrow-border); + color: var(--color-eyebrow-text); +} + +.website-eyebrow--on-dark { + background: rgba(159, 232, 112, 0.13); + border: 1px solid rgba(159, 232, 112, 0.28); + color: var(--color-sulu); +} + +/* Buttons */ +.website-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 15px 26px; + border-radius: var(--radius-lg); + font-size: 15.5px; + font-weight: 700; + letter-spacing: -0.01em; + border: 1px solid transparent; + cursor: pointer; + transition: + transform var(--ease-hover), + filter var(--ease-hover), + background-color var(--ease-hover), + color var(--ease-hover); + white-space: nowrap; +} + +.website-btn:hover { + transform: translateY(-1px); + filter: brightness(1.04); +} + +.website-body a.website-btn--primary { + background: var(--color-deep-fir); + color: #ffffff; +} + +.website-body a.website-btn--primary:hover { + background: var(--color-deep-fir); + color: #ffffff; + filter: none; +} + +.website-body a.website-btn--secondary { + background: var(--color-panel); + border-color: var(--color-border); + color: var(--color-ink); +} + +.website-body a.website-btn--secondary:hover { + color: var(--color-ink); +} + +.website-body a.website-btn.website-btn--nav { + padding: 11px 20px; + border-radius: 10px; + font-size: 15px; + font-weight: 700; + line-height: 1.2; + min-height: 42px; + height: 42px; + box-sizing: border-box; +} + +.website-btn--nav.website-btn--primary { + background: var(--color-deep-fir); + color: #ffffff; +} + +.website-body a.website-btn--nav.website-btn--primary { + color: #ffffff; +} + +.website-body a.website-btn--lime { + background: var(--color-sulu); + color: var(--color-deep-fir); +} + +.website-body a.website-btn--lime:hover { + color: var(--color-deep-fir); +} + +.website-body a.website-btn--cta-light { + background: rgba(255, 255, 255, 0.55); + color: var(--color-deep-fir); +} + +.website-body a.website-btn--cta-light:hover { + color: var(--color-deep-fir); +} + +.website-body a.website-btn--ghost-on-dark { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.22); + color: #ffffff; +} + +.website-body a.website-btn--ghost-on-dark:hover { + background: rgba(255, 255, 255, 0.16); + color: #ffffff; + filter: none; +} + +.website-btn--compact { + padding: 12px 22px; + border-radius: var(--radius-md); + font-size: 14px; + font-weight: 750; +} + +.website-btn--cta { + padding: 16px 30px; + font-size: 15.5px; + font-weight: 750; +} + +.website-live-dot--small { + width: 7px; + height: 7px; + background: var(--color-sulu); +} + +.website-live-dot--sulu { + background: var(--color-sulu); + animation: website-live-pulse 1.8s infinite; +} + +/* Section eyebrow */ +.website-section-eyebrow { + display: inline-block; + font-size: 12.5px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #5a8a3f; +} + +.website-section-eyebrow--on-dark { + color: var(--color-sulu); +} + +/* Step badge */ +.website-step-badge { + display: inline-flex; + align-items: center; + justify-content: center; + height: 26px; + padding: 0 11px; + border-radius: 999px; + background: var(--color-deep-fir); + color: var(--color-sulu); + font-family: var(--font-display); + font-weight: 750; + font-size: 12px; + letter-spacing: 0.01em; + line-height: 1; + white-space: nowrap; + flex: none; +} + +/* Status pill */ +.website-status-pill { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 11.5px; + font-weight: 700; + padding: 6px 13px; + border-radius: var(--radius-pill); +} + +.website-status-pill--live { + background: var(--color-sulu); + color: var(--color-deep-fir); +} + +.website-status-pill--tilt { + /* kept for optional use; no default tilt */ +} + +.website-status-pill__dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-deep-fir); +} + +/* Media placeholder */ +.website-media-placeholder { + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(145deg, #eef2e9 0%, #e2e8da 100%); + border: 1px dashed #cdd6c1; + color: var(--color-muted); + text-align: center; +} + +.website-media-placeholder__label { + font-size: 13px; + font-weight: 600; + line-height: 1.45; + max-width: 12rem; + padding: 1rem; +} + +/* Form mock (call-me preview) */ +.website-form-mock { + display: flex; + flex-direction: column; + gap: 13px; +} + +.website-form-mock__field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.website-form-mock__label { + font-size: 12px; + font-weight: 650; + color: var(--color-body); +} + +.website-form-mock__input { + padding: 12px 14px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 14px; + background: #fbfdf9; +} + +.website-form-mock__input--muted { + color: var(--color-muted); +} + +.website-form-mock__input--active { + border-color: #b7d69a; + color: var(--color-deep-fir); + font-weight: 650; +} + +.website-form-mock__submit { + width: 100%; + justify-content: center; + margin-top: 4px; +} + +.website-form-mock__hint { + margin: 2px 0 0; + font-size: 11.5px; + color: var(--color-muted); + text-align: center; +} + +/* Real forms */ +.website-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.website-form__row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +.website-form__field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.website-form__field.is-hidden, +.website-form__field[hidden] { + display: none; +} + +.website-form__label { + font-size: 12.5px; + font-weight: 650; + color: #3a4a38; +} + +.website-form__required { + color: #e5484d; +} + +.website-form__input, +.website-form__select { + width: 100%; + padding: 13px 14px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 14px; + background: #fbfdf9; + color: var(--color-deep-fir); + font-family: inherit; +} + +.website-form__input::placeholder { + color: #9aa697; +} + +.website-form__phone { + display: flex; + align-items: stretch; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: #fbfdf9; + overflow: hidden; +} + +.website-form__phone-prefix { + display: inline-flex; + align-items: center; + padding: 0 12px; + border-right: 1px solid var(--color-border); + color: var(--color-body); + font-size: 14px; + font-weight: 650; + background: #eef4e6; + flex: none; +} + +.website-form__phone-input { + border: none; + border-radius: 0; + background: transparent; +} + +.website-form__phone-input:focus { + outline: none; +} + +.website-form__phone:focus-within { + border-color: #b7d69a; + box-shadow: 0 0 0 3px rgba(159, 232, 112, 0.25); +} + +.website-form__input--error, +.website-form__select.website-form__input--error { + border-color: #e5484d; +} + +.website-form__error { + margin: 0; + font-size: 12px; + color: #e5484d; +} + +.website-form__footnote, +.website-form__hint { + margin: 0; + font-size: 12.5px; + color: var(--color-muted); + text-align: center; +} + +.website-form__banner { + margin-top: 20px; + padding: 14px 16px; + border-radius: var(--radius-md); + font-size: 14px; + line-height: 1.5; +} + +.website-form__banner--success { + background: var(--color-eyebrow-bg); + border: 1px solid var(--color-eyebrow-border); + color: var(--color-eyebrow-text); +} + +.website-form__banner--error { + background: #fff0f0; + border: 1px solid #f3c4c4; + color: #9b1c1c; +} + +.website-flash { + padding: 12px var(--site-gutter); + font-size: 14px; + font-weight: 600; + text-align: center; +} + +.website-flash--success { + background: var(--color-eyebrow-bg); + color: var(--color-eyebrow-text); +} + +.website-flash--danger { + background: #fff0f0; + color: #9b1c1c; +} + +/* Chat mock */ +.website-chat-mock { + background: var(--color-deep-fir); + border-radius: 18px; + padding: 20px 20px 18px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.website-chat-mock__toolbar { + display: flex; + align-items: center; + justify-content: space-between; + flex: none; +} + +.website-chat-mock__messages { + display: flex; + flex-direction: column; + gap: 10px; +} + +.website-chat-mock__live { + display: inline-flex; + align-items: center; + gap: 8px; + background: rgba(159, 232, 112, 0.14); + color: var(--color-sulu); + font-size: 11.5px; + font-weight: 700; + padding: 6px 12px; + border-radius: var(--radius-pill); +} + +.website-chat-mock__timer { + font-size: 11.5px; + font-weight: 650; + color: var(--color-on-dark-accent); + font-variant-numeric: tabular-nums; +} + +.website-chat-bubble { + margin: 0; + max-width: 86%; + font-size: 13px; + line-height: 1.5; + padding: 10px 14px; +} + +.website-chat-bubble--caller { + align-self: flex-start; + background: rgba(255, 255, 255, 0.1); + color: #e4f0d8; + border-radius: 14px 14px 14px 4px; +} + +.website-chat-bubble--agent { + align-self: flex-end; + background: var(--color-sulu); + color: var(--color-deep-fir); + font-weight: 550; + border-radius: 14px 14px 4px 14px; +} + +/* Checklist */ +.website-checklist { + display: flex; + gap: 18px; + margin: 20px 0 0; + padding: 0; + list-style: none; + font-size: 13px; + font-weight: 650; + color: #5a8a3f; + flex-wrap: wrap; +} + +/* Nav text link */ +.website-body a.website-nav-link { + font-size: 14px; + font-weight: 600; + color: var(--color-on-dark-muted); + padding: 11px 6px; + text-decoration: underline; + text-decoration-color: currentColor; + text-underline-offset: 0.42em; + text-decoration-thickness: 1px; + transition: + color var(--ease-hover), + text-decoration-color var(--ease-hover); +} + +.website-body a.website-nav-link:hover { + color: #ffffff; + text-decoration-color: #ffffff; +} + +.website-body a.website-nav-anchor { + font-size: 14px; + font-weight: 600; + color: var(--color-on-dark-muted); + padding: 11px 0; + border-radius: 0; + transition: color var(--ease-hover); +} + +.website-body a.website-nav-anchor:hover { + background: transparent; + color: #ffffff; +} diff --git a/static/css/website/demo.css b/static/css/website/demo.css new file mode 100644 index 0000000..f8d3552 --- /dev/null +++ b/static/css/website/demo.css @@ -0,0 +1,267 @@ +/* + * Book-a-demo page (demo-v2). + */ + +.website-body--demo { + background: #f7f9f4; + height: 100vh; + overflow: hidden; +} + +.website-site--demo { + height: 100vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.website-demo { + display: grid; + grid-template-columns: 46fr 54fr; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.website-demo__brand-panel { + background: var(--color-deep-fir); + color: #ffffff; + padding: 28px 48px 28px; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; +} + +.website-demo__brand-top { + display: flex; + align-items: center; +} + +.website-body a.website-demo__back-link { + font-size: 13.5px; + font-weight: 600; + color: rgba(255, 255, 255, 0.72); +} + +.website-body a.website-demo__back-link:hover { + color: #ffffff; +} + +.website-demo__brand-copy { + margin-top: 36px; +} + +.website-demo__title { + font-family: var(--font-display); + font-size: 40px; + line-height: 1.06; + font-weight: 800; + letter-spacing: -0.028em; + margin: 16px 0 0; + color: #ffffff; + max-width: 480px; +} + +.website-demo__title-accent { + color: var(--color-sulu); +} + +.website-demo__lead { + font-size: 15px; + line-height: 1.55; + color: var(--color-on-dark-muted); + margin: 14px 0 0; + max-width: 440px; +} + +.website-demo__metrics { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-top: 28px; +} + +.website-demo__metric { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.13); + border-radius: 14px; + padding: 16px 14px; + text-align: center; +} + +.website-demo__metric-value { + font-family: var(--font-display); + font-size: 28px; + font-weight: 800; + letter-spacing: -0.02em; + color: var(--color-sulu); +} + +.website-demo__metric-label { + font-size: 12.5px; + font-weight: 600; + color: var(--color-on-dark-muted); + margin-top: 6px; + line-height: 1.45; +} + +.website-demo__on-demo { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 16px; + padding: 20px 22px; + margin-top: auto; +} + +.website-demo__on-demo-label { + font-size: 11.5px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; + color: var(--color-on-dark-accent); + margin: 0 0 14px; +} + +.website-demo__on-demo-list { + display: flex; + flex-direction: column; + gap: 11px; + margin: 0; + padding: 0; + list-style: none; +} + +.website-demo__on-demo-list li { + display: flex; + gap: 11px; + align-items: flex-start; + font-size: 14px; + line-height: 1.5; + color: #e4f0d8; +} + +.website-demo__on-demo-list span { + color: var(--color-sulu); + font-weight: 800; +} + +.website-demo__founder { + display: flex; + align-items: center; + gap: 13px; + margin-top: 18px; + padding-top: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} + +.website-demo__founder-photo { + width: 46px; + height: 46px; + border-radius: 50%; + flex: none; +} + +.website-demo__founder-copy { + font-size: 13px; + line-height: 1.5; + color: var(--color-on-dark-muted); + margin: 0; +} + +.website-demo__founder-copy strong { + color: #ffffff; + font-weight: 650; +} + +.website-demo__form-panel { + position: relative; + overflow-x: hidden; + overflow-y: auto; + background: #f7f9f4; + padding: 32px 60px 44px; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +} + +.website-demo__support { + position: relative; + text-align: right; + font-size: 13px; + color: var(--color-muted); + font-weight: 550; + margin: 0; +} + +.website-demo__support a { + font-weight: 650; +} + +.website-demo__form-wrap { + position: relative; + max-width: 600px; + margin: 44px auto 0; + width: 100%; +} + +.website-demo__form-header { + display: flex; + align-items: center; + gap: 12px; +} + +.website-demo__form-title { + font-family: var(--font-display); + font-size: 34px; + font-weight: 800; + letter-spacing: -0.025em; + color: var(--color-deep-fir); + margin: 0; +} + +.website-demo__form-status { + display: flex; + align-items: center; + gap: 8px; + margin: 10px 0 0; + font-size: 14px; + font-weight: 600; + color: #5a8a3f; +} + +.website-demo__form-status-icon { + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--color-eyebrow-bg); + border: 1px solid #c8dbb4; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 10px; + color: var(--color-eyebrow-text); +} + +.website-demo__form-card { + background: #ffffff; + border: 1px solid var(--color-border-soft); + border-radius: 18px; + padding: 30px 28px; + margin-top: 26px; + box-shadow: 0 24px 50px -34px rgba(10, 31, 10, 0.3); +} + +.website-btn--demo-submit { + width: 100%; + justify-content: center; + padding: 15px; + border-radius: var(--radius-lg); + font-size: 15.5px; + font-weight: 750; + margin-top: 4px; + border: none; + cursor: pointer; +} diff --git a/static/css/website/login.css b/static/css/website/login.css new file mode 100644 index 0000000..8f799c7 --- /dev/null +++ b/static/css/website/login.css @@ -0,0 +1,400 @@ +/* + * Calliflowr branded login (email + password). + */ + +.cf-login-body { + margin: 0; + min-height: 100vh; + font-family: "Instrument Sans", var(--font-body); + color: var(--color-ink); + background: var(--color-page); + -webkit-font-smoothing: antialiased; +} + +.cf-login { + display: grid; + grid-template-columns: 480px minmax(0, 1fr); + min-height: 100vh; + background: var(--color-page); +} + +.cf-login__brand { + position: relative; + background: var(--color-deep-fir); + padding: 48px 52px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.cf-login-body a.cf-login__brand-mark { + font-family: var(--font-display); + font-weight: 800; + font-size: 22px; + letter-spacing: -0.02em; + color: #ffffff; + text-decoration: none; +} + +.cf-login-body a.cf-login__brand-mark:hover { + color: #ffffff; + text-decoration: none; +} + +.cf-login__brand-copy { + margin-top: auto; + position: relative; + z-index: 2; +} + +.cf-login__live { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--color-deep-fir); + border: 1px solid rgba(159, 232, 112, 0.35); + color: var(--color-sulu); + font-size: 12px; + font-weight: 700; + padding: 8px 14px; + border-radius: var(--radius-pill); +} + +.cf-login__live-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-sulu); + animation: website-live-pulse 1.6s infinite; +} + +.cf-login__brand-title { + font-family: var(--font-display); + font-size: 34px; + line-height: 1.14; + font-weight: 800; + letter-spacing: -0.025em; + color: #ffffff; + margin: 20px 0 0; +} + +.cf-login__brand-highlight { + background: var(--color-sulu); + color: var(--color-deep-fir); + padding: 0 10px 3px; + border-radius: 10px; + display: inline-block; + line-height: 1.2; + vertical-align: bottom; +} + +.cf-login__brand-lead { + font-size: 15px; + line-height: 1.65; + color: var(--color-on-dark-muted); + margin: 16px 0 0; + max-width: 320px; +} + +.cf-login__stats { + display: flex; + gap: 22px; + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid rgba(255, 255, 255, 0.14); +} + +.cf-login__stat-value { + font-family: var(--font-display); + font-size: 22px; + font-weight: 800; + color: var(--color-sulu); +} + +.cf-login__stat-label { + font-size: 12.5px; + font-weight: 550; + color: var(--color-on-dark-accent); + margin-top: 2px; +} + +.cf-login__glow { + position: absolute; + right: -120px; + bottom: -140px; + width: 340px; + height: 340px; + border-radius: 50%; + background: rgba(159, 232, 112, 0.08); + pointer-events: none; +} + +.cf-login__glow--soft { + right: -40px; + bottom: -200px; + background: rgba(159, 232, 112, 0.06); +} + +.cf-login__panel { + display: flex; + align-items: center; + justify-content: center; + padding: 48px 40px; +} + +.cf-login__card { + width: min(400px, 100%); +} + +.cf-login__title { + font-family: var(--font-display); + font-size: 30px; + font-weight: 800; + letter-spacing: -0.025em; + color: var(--color-deep-fir); + margin: 0; +} + +.cf-login__lead { + font-size: 14.5px; + line-height: 1.6; + color: var(--color-body); + margin: 10px 0 0; +} + +.cf-login__error { + margin-top: 20px; + padding: 12px 16px; + border-radius: 11px; + background: #fdf0ee; + border: 1px solid #f0cfc8; + color: #8c3a2b; + font-size: 13.5px; + font-weight: 550; +} + +.cf-login__warning { + margin-top: 20px; + padding: 12px 16px; + border-radius: 11px; + background: #fff8e8; + border: 1px solid #edd9a8; + color: #7a5a12; + font-size: 13.5px; + font-weight: 550; +} + +.cf-login__form { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 28px; +} + +.cf-login__field { + display: flex; + flex-direction: column; + gap: 7px; +} + +.cf-login__label { + font-size: 13px; + font-weight: 650; + color: var(--color-deep-fir); +} + +.cf-login__input { + padding: 13px 16px; + border-radius: 11px; + border: 1px solid var(--color-border); + background: #ffffff; + font-family: inherit; + font-size: 14.5px; + color: var(--color-deep-fir); + outline: none; +} + +.cf-login__input::placeholder { + color: #a3ac9f; +} + +.cf-login__input:focus { + border-color: var(--color-deep-fir); + box-shadow: 0 0 0 3px rgba(159, 232, 112, 0.35); +} + +.cf-login__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.cf-login__remember { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13.5px; + font-weight: 550; + color: var(--color-body); + cursor: pointer; +} + +.cf-login__remember input { + width: 16px; + height: 16px; + accent-color: var(--color-deep-fir); +} + +.cf-login-body a.cf-login__forgot { + font-size: 13.5px; + font-weight: 650; + color: var(--color-deep-fir); +} + +.cf-login-body a.cf-login__forgot:hover { + color: #2f5c14; +} + +.cf-login__back { + margin: 24px 0 0; + font-size: 13.5px; + font-weight: 550; + color: var(--color-body); + text-align: center; +} + +.cf-login__submit { + appearance: none; + border: none; + cursor: pointer; + text-align: center; + padding: 14px; + border-radius: 12px; + font-family: inherit; + font-size: 15px; + font-weight: 700; + letter-spacing: -0.01em; + background: var(--color-deep-fir); + color: #ffffff; + transition: transform var(--ease-hover); +} + +.cf-login__submit:hover:not(:disabled) { + transform: translateY(-1px); + color: #ffffff; +} + +.cf-login__submit:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.cf-login__note { + margin-top: 28px; + padding: 14px 18px; + border-radius: 12px; + background: #eef4e6; + border: 1px solid #dfe7d6; + font-size: 13px; + line-height: 1.6; + color: #3a4a38; +} + +.cf-login__note strong { + color: var(--color-deep-fir); +} + +.cf-login-body a.cf-login__note-link { + font-weight: 700; + color: var(--color-deep-fir); +} + +.cf-login-body a.cf-login__note-link:hover { + color: #2f5c14; +} + +.cf-login__dashboard-link { + display: block; + margin-top: 24px; + text-align: center; + padding: 14px; + border-radius: 12px; + font-size: 15px; + font-weight: 700; + background: var(--color-deep-fir); + color: #ffffff; +} + +.cf-login-body a.cf-login__dashboard-link:hover { + color: #ffffff; + transform: translateY(-1px); +} + +/* Toast feedback for failed login (HTMX OOB) */ +.cf-login-body .toast-container { + position: fixed; + z-index: 1100; + max-width: 24rem; +} + +.cf-login-body .toast-container.bottom-0 { bottom: 0; } +.cf-login-body .toast-container.end-0 { right: 0; } +.cf-login-body .toast-container.p-3 { padding: 1rem; } + +.cf-login-body .toast { + width: 100%; + border-radius: 12px; + color: #fff; + box-shadow: 0 12px 28px -16px rgba(10, 31, 10, 0.45); + animation: fjp-toast-in 0.2s ease; +} + +.cf-login-body .toast:not(.show) { display: none; } +.cf-login-body .toast .d-flex { display: flex; align-items: stretch; } +.cf-login-body .toast-body { flex: 1 1 auto; padding: 0.75rem 0.9rem; font-size: 14px; font-weight: 550; } +.cf-login-body .toast.text-bg-danger { background: #9b1c1c; } +.cf-login-body .toast.text-bg-success { background: #245a12; } +.cf-login-body .toast.text-bg-warning { background: #a16207; color: #fff; } +.cf-login-body .btn-close { + appearance: none; + border: 0; + background: transparent; + color: #fff; + font-size: 1.1rem; + line-height: 1; + padding: 0.75rem; + cursor: pointer; + opacity: 0.85; +} +.cf-login-body .btn-close::before { content: "×"; } + +@media (max-width: 900px) { + .cf-login { + grid-template-columns: 1fr; + } + + .cf-login__brand { + min-height: auto; + padding: 28px 24px 36px; + } + + .cf-login__brand-copy { + margin-top: 36px; + } + + .cf-login__brand-title { + font-size: 28px; + } + + .cf-login__glow, + .cf-login__glow--soft { + display: none; + } + + .cf-login__panel { + padding: 36px 22px 48px; + align-items: flex-start; + } +} diff --git a/static/css/website/responsive.css b/static/css/website/responsive.css new file mode 100644 index 0000000..0cfa939 --- /dev/null +++ b/static/css/website/responsive.css @@ -0,0 +1,806 @@ +/* + * Website responsive layout — tablet + mobile (v3-mobile.dc.html parity). + */ + +/* --- Tablet: collapse multi-column sections --- */ + +@media (max-width: 1100px) { + .website-hero { + grid-template-columns: 1fr; + gap: 48px; + padding: 48px 40px 64px; + } + + .website-hero__showcase { + width: 100%; + max-width: 520px; + justify-self: center; + } + + .website-hero__live { + right: 0; + } + + .website-hero__bubble { + left: 8px; + } + + .website-nav { + grid-template-columns: auto 1fr auto; + padding: 16px 40px; + } + + .website-nav__links { + margin-left: 12px; + gap: 18px; + } + + .website-footer__main { + grid-template-columns: 1fr 1fr; + gap: 36px; + } + + .website-call-me { + margin: 0; + padding: 64px 40px; + border-radius: 0; + } + + .website-how__grid { + grid-template-columns: 1fr; + } + + .website-agents__grid { + grid-template-columns: 1fr; + } + + .website-faq__grid { + grid-template-columns: 1fr; + gap: 32px; + } + + .website-call-me__grid { + grid-template-columns: 1fr; + gap: 36px; + } + + .website-math__grid { + grid-template-columns: 1fr; + gap: 72px; + } + + .website-math__copy { + max-width: 660px; + } + + .website-comparison__scroll { + border-radius: 20px; + } + + .website-demo { + grid-template-columns: 1fr; + flex: none; + height: auto; + min-height: 0; + overflow: visible; + } + + .website-body--demo, + .website-site--demo { + height: auto; + overflow: visible; + display: block; + } + + .website-demo__brand-panel { + height: auto; + overflow: visible; + padding: 28px var(--site-gutter) 36px; + } + + .website-demo__form-panel { + height: auto; + overflow: visible; + padding: 28px var(--site-gutter) 36px; + } + + .website-demo__metrics { + grid-template-columns: 1fr; + } + + .website-demo__title { + font-size: 38px; + } +} + +/* --- Mobile: 390px artboard parity --- */ + +@media (max-width: 768px) { + :root { + --site-gutter: 22px; + --section-gutter: 22px; + } + + /* Nav — logo + Book a Demo only */ + .website-nav { + display: flex; + min-height: 65px; + height: auto; + gap: 12px; + padding: 14px 20px; + } + + .website-body a.website-nav__brand { + font-size: 20px; + } + + .website-nav__links, + .website-nav__actions .website-nav-link { + display: none; + } + + .website-nav__actions { + margin-left: auto; + justify-self: unset; + gap: 0; + } + + .website-btn--nav { + padding: 11px 16px; + border-radius: 10px; + font-size: 14px; + font-weight: 700; + min-height: 40px; + height: 40px; + } + + /* Content pages */ + .website-body--content .website-nav--simple { + min-height: 52px; + padding: 8px 22px; + } + + .website-content-page { + padding: 36px 22px 48px; + } + + .website-content-page__title { + font-size: 36px; + } + + .website-content-page__body { + font-size: 15px; + margin-top: 20px; + } + + /* Hero */ + .website-hero { + padding: 36px 22px 48px; + gap: 36px; + min-height: 0; + } + + .website-eyebrow { + gap: 8px; + padding: 6px 13px; + font-size: 11.5px; + } + + .website-live-dot { + width: 7px; + height: 7px; + } + + .website-hero__title { + font-size: 40px; + line-height: 1.06; + letter-spacing: -0.03em; + margin-top: 0; + max-width: none; + } + + .website-hero__highlight { + padding: 0 10px 5px; + border-radius: 11px; + } + + .website-hero__lead { + font-size: 15px; + margin-top: 16px; + max-width: 320px; + } + + .website-hero__actions { + flex-direction: column; + align-items: stretch; + gap: 10px; + margin-top: 24px; + } + + .website-hero__actions .website-btn { + width: auto; + max-width: none; + height: 40px; + min-height: 40px; + padding: 11px 16px; + justify-content: center; + font-size: 14px; + border-radius: 10px; + } + + .website-body a.website-hero__text-link { + align-self: center; + font-size: 15px; + padding: 8px 2px; + } + + .website-hero__trust { + font-size: 12px; + margin-top: 18px; + } + + .website-hero__showcase { + width: 100%; + max-width: none; + } + + .website-hero__stage { + width: 100%; + height: auto; + aspect-ratio: 560 / 512; + margin: 8px 0 0; + } + + .website-hero__plate--lime, + .website-hero__plate--glass { + display: none; + } + + .website-hero__live { + right: 4px; + top: 18px; + font-size: 11px; + padding: 8px 12px; + } + + .website-hero__bubble { + left: 4px; + bottom: 72px; + font-size: 11.5px; + padding: 9px 12px; + border-radius: 10px; + white-space: normal; + max-width: 200px; + gap: 7px; + } + + .website-hero__trust-icon { + width: 14px; + height: 18px; + } + + .website-hero__card-image, + .website-hero__card-image.website-media-placeholder { + height: 100%; + min-height: 0; + } + + .website-hero__genres { + margin-top: 32px; + overflow-x: auto; + scrollbar-width: none; + } + + .website-hero__genres::-webkit-scrollbar { + display: none; + } + + .website-hero__genre { + flex: none; + padding: 9px 14px; + font-size: 12px; + } + + /* How it works */ + .website-how { + margin-top: 0; + padding: 64px 18px 56px; + border-radius: 0; + } + + .website-how__title { + font-size: 31px; + line-height: 1.1; + margin-top: 12px; + max-width: 300px; + } + + .website-how__grid { + margin-top: 34px; + gap: 14px; + } + + .website-how-step { + border-radius: 22px; + } + + .website-how-step--lime, + .website-how-step--panel { + padding: 24px 22px; + } + + .website-how-step--panel { + padding-bottom: 0; + } + + .website-how-step--dashboard { + padding: 24px 16px 28px; + } + + .website-how-step__media--dashboard { + transform: none; + } + + .website-how-step--booked-card { + flex-direction: column; + align-items: stretch; + gap: 18px; + padding: 20px 18px 20px; + } + + .website-how-step--booked-card .website-how-step__copy { + max-width: none; + padding-top: 4px; + padding-right: 0; + } + + .website-how-step--booked-card .website-how-step__title { + white-space: normal; + font-size: 22px; + } + + .website-how-step--booked-card .website-booking-visual { + justify-content: flex-end; + } + + .website-how__grid--follow { + margin-top: 14px; + } + + .website-step-badge { + height: 24px; + padding: 0 10px; + font-size: 11.5px; + } + + .website-how-step__header { + gap: 11px; + } + + .website-how-step__title { + font-size: 22px; + white-space: normal; + } + + .website-how-step__lead { + font-size: 14px; + margin-top: 12px; + max-width: none; + } + + .website-how-step__lead--on-lime { + max-width: none; + } + + .website-how-step__lead { + min-height: 0; + } + + .website-how-step--lime .website-chat-mock { + margin-top: 0; + padding-top: 0; + border-radius: 16px; + padding: 16px; + } + + .website-how-step__media--chat { + margin-top: 22px; + padding-top: 0; + } + + .website-how-step__screenshot, + .website-how-step__media--dashboard .website-media-placeholder { + width: 100%; + margin-top: 0; + border-radius: 12px; + } + + .website-booking-visual { + margin-bottom: 0; + } + + .website-booking-visual__email, + .website-booking-visual .website-media-placeholder { + width: min(320px, 100%); + min-height: 0; + height: auto; + border-radius: 14px; + border: 1px solid var(--color-border-soft); + } + + /* Agents */ + .website-agents { + padding: 44px 18px 50px; + } + + .website-agents__title { + font-size: 29px; + margin-top: 10px; + max-width: none; + } + + .website-agents__grid { + margin-top: 26px; + gap: 14px; + } + + .website-agent-card { + border-radius: 20px; + padding: 24px 22px; + } + + .website-agent-card__watermark { + font-size: 180px; + right: -24px; + bottom: -52px; + } + + .website-agent-card--coming_soon .website-agent-card__watermark { + right: -18px; + } + + .website-agent-card__header { + gap: 11px; + } + + .website-agent-card__avatar { + width: 44px; + height: 44px; + font-size: 19px; + } + + .website-agent-card__name { + font-size: 20px; + } + + .website-agent-card__role { + font-size: 12px; + } + + .website-agent-card__status { + padding: 5px 11px; + font-size: 10.5px; + } + + .website-agent-card__status--coming_soon { + font-size: 10px; + } + + .website-agent-card__description { + font-size: 13.5px; + margin: 14px 0; + } + + .website-agent-card__feature { + font-size: 13.5px; + gap: 10px; + } + + .website-agent-card__cta { + margin-top: 20px; + width: 100%; + justify-content: center; + } + + /* Call me */ + .website-call-me { + margin: 0; + padding: 48px 22px 52px; + border-radius: 0; + } + + .website-call-me__grid { + gap: 0; + } + + .website-call-me__title { + font-size: 29px; + line-height: 1.12; + margin-top: 12px; + max-width: none; + } + + .website-call-me__lead { + font-size: 14.5px; + margin-top: 14px; + max-width: none; + } + + .website-call-me__bullets { + flex-wrap: wrap; + gap: 14px; + margin-top: 18px; + font-size: 12.5px; + } + + .website-call-me__card { + margin-top: 26px; + border-radius: 16px; + padding: 22px 20px; + } + + .website-call-me__card-title { + font-size: 16px; + margin-bottom: 16px; + } + + /* FAQ */ + .website-faq { + padding: 48px 22px; + } + + .website-faq__grid { + gap: 0; + } + + .website-faq__title { + font-size: 29px; + line-height: 1.1; + margin-top: 12px; + } + + .website-faq__aside--desktop { + display: none; + } + + .website-faq__aside--mobile { + display: block; + } + + .website-faq__list { + margin-top: 10px; + } + + .website-faq__item { + padding: 18px 0; + } + + .website-faq__question { + font-size: 15.5px; + } + + .website-faq__answer { + font-size: 13.5px; + margin-top: 8px; + max-width: none; + } + + /* Final CTA */ + .website-final-cta { + padding: 40px 22px 48px; + } + + .website-final-cta__panel { + flex-direction: column; + align-items: stretch; + border-radius: 28px; + padding: 24px 24px 24px; + gap: 28px; + } + + .website-final-cta__copy { + padding-bottom: 0; + } + + .website-final-cta__title { + font-size: 33px; + line-height: 1.08; + max-width: none; + } + + .website-final-cta__lead { + font-size: 14.5px; + margin-top: 14px; + max-width: none; + } + + .website-final-cta__actions { + flex-direction: column; + align-items: stretch; + gap: 10px; + margin-top: 24px; + } + + .website-final-cta__actions .website-btn { + justify-content: center; + padding: 15px; + font-size: 15px; + font-weight: 750; + border-radius: var(--radius-lg); + } + + .website-final-cta__media { + justify-content: center; + padding-top: 0; + padding-right: 0; + } + + .website-final-cta__phone { + width: min(280px, 72vw); + border-radius: 24px; + padding: 8px; + } + + /* Brand strip */ + .website-brand-strip { + padding: 22px 22px; + } + + .website-brand-strip__inner { + gap: 12px; + } + + .website-body a.website-brand-strip__wordmark { + font-size: 22px; + } + + .website-brand-strip__tagline { + font-size: 14.5px; + } + + /* Footer */ + .website-footer { + padding: 44px 22px 22px; + } + + .website-footer__main { + grid-template-columns: 1fr; + gap: 28px; + } + + .website-footer__column-title { + font-size: 15px; + margin-bottom: 16px; + } + + .website-footer__nav { + gap: 14px; + font-size: 15.5px; + } + + .website-footer__industries { + gap: 14px; + font-size: 15.5px; + } + + .website-footer__legal { + flex-direction: column; + align-items: flex-start; + gap: 8px; + margin-top: 48px; + padding-top: 22px; + font-size: 15.5px; + } + + /* Demo page */ + .website-form__row { + grid-template-columns: 1fr; + } + + .website-demo__brand-copy { + margin-top: 36px; + } + + .website-demo__form-wrap { + margin-top: 28px; + } + + .website-demo__form-title { + font-size: 28px; + } + + /* Flash banner */ + .website-flash { + padding: 12px 20px; + font-size: 13px; + } + + /* Missed-call calculator */ + .website-math { + padding: 64px 22px 48px; + } + + .website-math__grid { + gap: 58px; + } + + .website-math__title { + font-size: 32px; + } + + .website-math__lead { + font-size: 14.5px; + } + + .website-math__controls { + gap: 24px; + margin-top: 34px; + } + + .website-math__control + .website-math__control { + padding-top: 24px; + } + + .website-math__control-heading { + align-items: flex-start; + gap: 14px; + } + + .website-math__hint { + max-width: 190px; + } + + .website-math__value { + font-size: 28px; + } + + .website-math-receipt { + width: min(340px, 94%); + } + + .website-math-receipt__paper { + padding: 36px 24px 26px; + } + + .website-math-receipt__total output { + font-size: 38px; + } + + .website-math__sources { + margin-top: 54px; + text-align: left; + } + + /* Honest comparison */ + .website-comparison { + padding: 64px 22px 72px; + } + + .website-comparison__title { + font-size: 32px; + } + + .website-comparison__lead { + margin-top: 16px; + font-size: 14.5px; + } + + .website-comparison__scroll { + margin-top: 40px; + border-radius: 18px; + } + + .website-comparison__table { + min-width: 920px; + } + + .website-comparison__table th, + .website-comparison__table td { + padding: 16px 18px; + } + + .website-comparison__footnote { + margin-top: 38px; + text-align: left; + } +} diff --git a/static/css/website/sections.css b/static/css/website/sections.css new file mode 100644 index 0000000..69cc650 --- /dev/null +++ b/static/css/website/sections.css @@ -0,0 +1,1724 @@ +/* + * Website section layouts (desktop). + */ + +/* --- Nav --- */ + +.website-nav { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 28px; + min-height: 72px; + padding: 18px 64px; + background: var(--color-deep-fir); +} + +.website-body a.website-nav__brand { + display: flex; + align-items: center; + font-family: var(--font-display); + font-weight: 800; + font-size: 21px; + letter-spacing: -0.02em; + color: #ffffff; + justify-self: start; +} + +.website-body a.website-nav__brand:hover { + color: #ffffff; +} + +.website-nav__links { + display: flex; + gap: 28px; + justify-self: start; + margin-left: 24px; +} + +.website-nav__actions { + justify-self: end; + display: flex; + align-items: center; + gap: 14px; +} + +.website-nav--simple { + grid-template-columns: auto 1fr; + min-height: 56px; + padding-top: 10px; + padding-bottom: 10px; +} + +.website-nav--simple .website-nav__actions { + justify-self: end; +} + +/* --- Hero --- */ + +.website-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 560px); + align-items: center; + gap: 64px; + text-align: left; + padding: 56px 64px 88px; + background: var(--color-deep-fir); + box-sizing: border-box; +} + +.website-hero__copy { + min-width: 0; +} + +.website-hero__copy .website-eyebrow { + margin: 0 0 18px; +} + +.website-hero__title { + font-family: var(--font-display); + font-size: 60px; + line-height: 1.06; + font-weight: 800; + letter-spacing: -0.032em; + margin: 0; + max-width: 560px; + color: #ffffff; +} + +.website-hero__highlight { + background: var(--color-sulu); + color: var(--color-deep-fir); + padding: 0 16px 6px; + border-radius: 15px; + display: inline-block; + line-height: 1.16; + vertical-align: bottom; +} + +.website-hero__lead { + font-size: 17.5px; + line-height: 1.65; + color: var(--color-on-dark-muted); + margin: 24px 0 0; + max-width: 440px; +} + +.website-hero__actions { + display: flex; + gap: 13px 22px; + margin-top: 32px; + flex-wrap: wrap; + align-items: center; +} + +.website-hero__actions .website-btn { + box-sizing: border-box; + width: auto; + height: 42px; + min-height: 42px; + padding: 11px 20px; + border-radius: 10px; + font-size: 15px; + font-weight: 700; + line-height: 1.2; + justify-content: center; +} + +.website-body a.website-hero__text-link { + display: inline-flex; + align-items: center; + align-self: center; + padding: 4px 2px; + font-size: 16.5px; + font-weight: 700; + letter-spacing: -0.01em; + color: rgba(255, 255, 255, 0.78); + text-decoration: underline; + text-decoration-color: rgba(255, 255, 255, 0.55); + text-underline-offset: 0.38em; + text-decoration-thickness: 1.5px; + transition: + color var(--ease-hover), + text-decoration-color var(--ease-hover); +} + +.website-body a.website-hero__text-link:hover { + color: #ffffff; + text-decoration-color: #ffffff; +} + +.website-hero__trust { + margin: 24px 0 0; + font-size: 13px; + font-weight: 550; + color: var(--color-on-dark-accent); +} + +.website-hero__showcase { + min-width: 0; + display: flex; + flex-direction: column; + justify-self: end; + width: min(100%, 560px); +} + +.website-hero__stage { + position: relative; + width: 100%; + height: 512px; +} + +.website-hero__plate { + position: absolute; + border-radius: 24px; + pointer-events: none; +} + +.website-hero__plate--lime { + inset: -18px -14px 26px auto; + width: 88%; + background: var(--color-sulu); + transform: rotate(2.8deg); +} + +.website-hero__plate--glass { + inset: 22px auto -14px -16px; + width: 88%; + background: rgba(233, 242, 224, 0.16); + border: 1px solid rgba(216, 228, 200, 0.25); + transform: rotate(-3.2deg); +} + +.website-hero__live { + position: absolute; + right: -18px; + top: 34px; + z-index: 4; + display: inline-flex; + align-items: center; + gap: 8px; + background: + linear-gradient(var(--color-deep-fir), var(--color-deep-fir)) padding-box, + linear-gradient(90deg, var(--color-sulu), var(--color-deep-fir)) border-box; + border: 2px solid transparent; + color: var(--color-sulu); + font-size: 12px; + font-weight: 700; + padding: 9px 15px; + border-radius: var(--radius-pill); + box-shadow: + 0 6px 12px -4px rgba(0, 0, 0, 0.45), + 0 20px 36px -12px rgba(0, 0, 0, 0.55); +} + +.website-hero__live-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-sulu); + animation: website-live-pulse 1.6s infinite; +} + +.website-hero__bubble { + position: absolute; + left: -32px; + bottom: 55px; + z-index: 4; + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--color-deep-fir); + font-size: 12.5px; + font-weight: 650; + line-height: 1.45; + padding: 10px 12px; + border-radius: 12px; + border: 2px solid transparent; + background: + linear-gradient(#ffffff, #ffffff) padding-box, + linear-gradient(90deg, var(--color-sulu), var(--color-deep-fir)) border-box; + box-shadow: + 0 6px 10px -3px rgba(10, 31, 10, 0.18), + 0 18px 36px -8px rgba(10, 31, 10, 0.38); + white-space: nowrap; +} + +.website-hero__trust-icon { + flex: none; + width: 16px; + height: 20px; + display: block; + background-color: var(--color-deep-fir); + -webkit-mask-image: url("../../img/website/icons/trusted.png"); + mask-image: url("../../img/website/icons/trusted.png"); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; +} + +.website-hero__card { + position: absolute; + inset: 0; + margin: 0; + display: flex; + flex-direction: column; + background: #ffffff; + border-radius: 16px; + padding: 12px; + box-shadow: + 0 32px 56px -26px rgba(0, 0, 0, 0.55), + 0 10px 22px -12px rgba(0, 0, 0, 0.35); + transform: rotate(2.8deg) scale(0.97); + opacity: 0; + pointer-events: none; + z-index: 1; + transition: + opacity 0.25s ease, + transform 0.25s ease; +} + +.website-hero__card.is-active { + opacity: 1; + pointer-events: auto; + z-index: 2; + transform: rotate(2.8deg) scale(1); +} + +.website-hero__card-image { + width: 100%; + height: 100%; + flex: 1 1 auto; + min-height: 0; + border-radius: 10px; + object-fit: cover; + display: block; + overflow: hidden; +} + +.website-hero__card-image.website-media-placeholder { + min-height: 0; +} + +.website-hero__genres { + position: relative; + z-index: 3; + display: flex; + gap: 3px; + margin-top: 52px; + background: #ffffff; + border: 1px solid #dfe7d6; + border-radius: var(--radius-pill); + padding: 5px; + overflow-x: auto; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + box-shadow: + 0 1px 2px rgba(10, 31, 10, 0.05), + 0 14px 30px -20px rgba(10, 31, 10, 0.35); +} + +.website-hero__genres::-webkit-scrollbar { + display: none; +} + +.website-hero__genre { + flex: 1 0 auto; + text-align: center; + white-space: nowrap; + border: none; + background: transparent; + color: var(--color-body); + border-radius: var(--radius-pill); + padding: 10px 12px; + font-size: 13px; + font-weight: 600; + letter-spacing: -0.005em; + font-family: inherit; + cursor: pointer; + transition: + background-color 0.18s ease, + color 0.18s ease, + box-shadow 0.18s ease; +} + +.website-hero__genre:hover { + background: #eef4e6; + color: var(--color-deep-fir); +} + +.website-hero__genre.is-active { + font-weight: 700; + background: var(--color-deep-fir); + color: var(--color-sulu); + box-shadow: 0 6px 14px -6px rgba(10, 31, 10, 0.45); +} + +.website-hero__genre.is-active:hover { + background: var(--color-deep-fir); + color: var(--color-sulu); +} + +/* --- How it works --- */ + +.website-how { + margin-top: 0; + position: relative; + padding: 100px 64px; + background: var(--color-page); + border-radius: 0; + text-align: center; +} + +.website-how__title { + font-family: var(--font-display); + font-weight: 800; + font-size: 40px; + line-height: 1.08; + letter-spacing: -0.025em; + color: var(--color-deep-fir); + margin: 14px auto 0; + max-width: 620px; +} + +.website-how__grid { + display: grid; + grid-template-columns: 0.88fr 1.12fr; + gap: 20px; + margin-top: 52px; + align-items: stretch; + text-align: left; +} + +.website-how__grid--follow { + margin-top: 20px; +} + +.website-how__grid--single { + grid-template-columns: 1fr; +} + +.website-how-step { + border-radius: 26px; + overflow: hidden; +} + +.website-how-step--lime { + background: var(--color-sulu); + padding: 34px; + display: flex; + flex-direction: column; +} + +.website-how-step--panel { + background: var(--color-panel); + padding: 34px 34px 0; + display: flex; + flex-direction: column; +} + +.website-how-step--dashboard { + padding: 34px 24px 40px; +} + +.website-how-step--alert { + padding: 34px; +} + +.website-how-step--booked-card { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + gap: 36px; + padding: 22px 22px 22px 28px; +} + +.website-how-step--booked-card .website-how-step__copy { + flex: 1 1 auto; + min-width: 0; + max-width: none; + padding-top: 10px; + padding-right: 12px; +} + +.website-how-step--booked-card .website-how-step__header { + flex-wrap: nowrap; + gap: 12px; +} + +.website-how-step--booked-card .website-how-step__title { + white-space: nowrap; + font-size: 24px; + line-height: 1.15; +} + +.website-how-step--booked-card .website-how-step__lead { + min-height: 0; + max-width: none; + width: 100%; +} + +.website-how-step--booked-card .website-how-step__lead--on-lime { + max-width: none; +} + +.website-how-step--booked-card .website-booking-visual { + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + margin-top: 0; + padding-top: 0; +} + +.website-how-step__header { + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: 13px; +} + +.website-how-step__title { + font-family: var(--font-display); + font-size: 24px; + font-weight: 800; + letter-spacing: -0.02em; + line-height: 1.15; + margin: 0; + color: var(--color-deep-fir); + white-space: nowrap; +} + +.website-how-step__lead { + font-size: 15px; + line-height: 1.6; + color: var(--color-body); + margin: 14px 0 0; + max-width: none; + width: 100%; + min-height: 0; +} + +.website-how-step__lead--on-lime { + color: #2f5a1b; + max-width: none; +} + +.website-emergency-pill { + display: inline-flex; + align-items: center; + align-self: flex-start; + gap: 7px; + margin-top: 18px; + background: #ffe9db; + color: #c2410c; + font-size: 11.5px; + font-weight: 700; + padding: 6px 11px; + border-radius: var(--radius-pill); +} + +.website-how-step--lime .website-chat-mock { + margin-top: 0; + padding-top: 20px; +} + +.website-how-step__media { + margin-top: auto; + padding-top: 24px; +} + +.website-how-step__media--chat { + padding-bottom: 0; +} + +.website-how-step__media--dashboard { + padding-top: 28px; + margin-top: auto; + margin-bottom: auto; + transform: translateY(2px); +} + +.website-how-step__screenshot { + display: block; + width: 100%; + height: auto; + margin-top: 0; + border-radius: 14px; + border: 1px solid var(--color-border-soft); + box-shadow: 0 12px 36px -20px rgba(10, 31, 10, 0.35); +} + +.website-how-step__media--dashboard .website-media-placeholder { + width: 100%; + min-height: 220px; + margin-top: 0; + border-radius: 14px; +} + +.website-booking-visual { + position: relative; + border-radius: 16px; +} + +.website-booking-visual__email { + display: block; + width: 390px; + max-width: 100%; + height: auto; + border-radius: 16px; + border: 1px solid var(--color-border-soft); + box-shadow: 0 20px 44px -26px rgba(10, 31, 10, 0.4); + overflow: hidden; +} + +.website-booking-visual .website-media-placeholder { + width: 390px; + max-width: 100%; + min-height: 480px; + border-radius: 16px; +} + + +/* --- Agents --- */ + +.website-agents { + padding: 100px 64px; + background: #ffffff; + border-top: 1px solid #e7ece1; +} + +.website-agents__intro { + text-align: center; +} + +.website-agents__title { + font-family: var(--font-display); + font-weight: 800; + font-size: 40px; + line-height: 1.08; + letter-spacing: -0.025em; + color: var(--color-deep-fir); + margin: 14px auto 0; + max-width: 640px; +} + +.website-agents__lead { + font-size: 15.5px; + line-height: 1.55; + color: var(--color-body); + margin: 16px auto 0; + max-width: 560px; +} + +.website-agents__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 20px; + margin-top: 48px; + align-items: stretch; +} + +.website-agent-card { + position: relative; + overflow: hidden; + border-radius: 22px; + padding: 28px 26px 26px; + display: flex; + flex-direction: column; +} + +.website-agent-card--live { + background: var(--color-deep-fir); + color: #ffffff; + box-shadow: 0 30px 60px -30px rgba(10, 31, 10, 0.55); +} + +.website-agent-card--in_pilot { + background: #f7faf3; + border: 1px solid #d7e2ca; +} + +.website-agent-card--coming_soon { + background: #f7faf3; + border: 1px dashed #cdd6c1; +} + +.website-agent-card__watermark { + position: absolute; + right: -28px; + bottom: -64px; + font-family: var(--font-display); + font-size: 200px; + font-weight: 800; + line-height: 1; + user-select: none; + pointer-events: none; +} + +.website-agent-card--live .website-agent-card__watermark { + color: rgba(159, 232, 112, 0.07); +} + +.website-agent-card--in_pilot .website-agent-card__watermark, +.website-agent-card--coming_soon .website-agent-card__watermark { + right: -20px; + color: rgba(10, 31, 10, 0.04); +} + +.website-agent-card__header { + position: relative; + display: flex; + align-items: flex-start; + gap: 12px; +} + +.website-agent-card__avatar { + width: 46px; + height: 46px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-family: var(--font-display); + font-weight: 800; + font-size: 20px; + flex: none; +} + +.website-agent-card__avatar--live { + background: var(--color-sulu); + color: var(--color-deep-fir); + animation: website-live-pulse 2.2s infinite; +} + +.website-agent-card--in_pilot .website-agent-card__avatar { + background: #d7ebc0; + color: var(--color-deep-fir); +} + +.website-agent-card--coming_soon .website-agent-card__avatar { + background: #dbe4d0; + color: #5a7a44; +} + +.website-agent-card__titles { + min-width: 0; + padding-top: 2px; +} + +.website-agent-card__name { + font-family: var(--font-display); + font-size: 22px; + font-weight: 700; + letter-spacing: -0.015em; + margin: 0; +} + +.website-agent-card--live .website-agent-card__name { + color: #ffffff; +} + +.website-agent-card--in_pilot .website-agent-card__name, +.website-agent-card--coming_soon .website-agent-card__name { + color: #3a4a38; +} + +.website-agent-card__channels { + margin: 3px 0 0; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.website-agent-card--live .website-agent-card__channels { + color: var(--color-sulu); +} + +.website-agent-card--in_pilot .website-agent-card__channels { + color: #5a8a3f; +} + +.website-agent-card--coming_soon .website-agent-card__channels { + color: var(--color-muted); +} + +.website-agent-card__status { + margin-left: auto; + font-size: 11.5px; + font-weight: 700; + padding: 6px 12px; + border-radius: var(--radius-pill); + white-space: nowrap; + flex: none; +} + +.website-agent-card__status--live { + display: inline-flex; + align-items: center; + gap: 7px; + background: var(--color-sulu); + color: var(--color-deep-fir); +} + +.website-agent-card__status--live::before { + content: ""; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-deep-fir); +} + +.website-agent-card__status--in_pilot { + background: #eaf5db; + border: 1px solid #c5dfa8; + color: #2f5a1b; + letter-spacing: 0.03em; + text-transform: uppercase; + font-size: 11px; +} + +.website-agent-card__status--coming_soon { + background: #eef2e9; + border: 1px dashed #c3cfb5; + color: #5a6957; + letter-spacing: 0.04em; + text-transform: uppercase; + font-size: 11px; +} + +.website-agent-card__description { + position: relative; + font-size: 14.5px; + line-height: 1.55; + margin: 16px 0; +} + +.website-agent-card--live .website-agent-card__description { + color: var(--color-on-dark-muted); +} + +.website-agent-card--in_pilot .website-agent-card__description, +.website-agent-card--coming_soon .website-agent-card__description { + color: #6b7669; +} + +.website-agent-card__features { + position: relative; + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 11px; + flex: 1; +} + +.website-agent-card__feature { + display: flex; + gap: 11px; + align-items: flex-start; + font-size: 14px; + line-height: 1.5; +} + +.website-agent-card--live .website-agent-card__feature { + color: #e4f0d8; +} + +.website-agent-card--live .website-agent-card__feature strong { + color: #ffffff; + font-weight: 650; +} + +.website-agent-card--in_pilot .website-agent-card__feature, +.website-agent-card--coming_soon .website-agent-card__feature { + color: #6b7669; +} + +.website-agent-card--in_pilot .website-agent-card__feature strong, +.website-agent-card--coming_soon .website-agent-card__feature strong { + color: #4a5647; + font-weight: 650; +} + +.website-agent-card__feature-symbol { + font-weight: 800; + flex: none; +} + +.website-agent-card--live .website-agent-card__feature-symbol { + color: var(--color-sulu); +} + +.website-agent-card--in_pilot .website-agent-card__feature-symbol { + color: #5a8a3f; +} + +.website-agent-card--coming_soon .website-agent-card__feature-symbol { + color: #9aa697; +} + +.website-agent-card__cta { + position: relative; + margin-top: 22px; + align-self: flex-start; +} + +/* --- Call me --- */ + +.website-call-me { + margin: 0; + padding: 100px 64px; + background: var(--color-deep-fir); + color: #ffffff; + border-radius: 0; +} + +.website-call-me__grid { + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: 64px; + align-items: center; +} + +.website-call-me__title { + font-family: var(--font-display); + font-weight: 800; + font-size: 40px; + line-height: 1.1; + letter-spacing: -0.025em; + color: #ffffff; + margin: 14px 0 0; +} + +.website-call-me__lead { + font-size: 16px; + line-height: 1.6; + color: var(--color-on-dark-muted); + margin: 18px 0 0; + max-width: 440px; +} + +.website-call-me__bullets { + display: flex; + gap: 22px; + margin: 26px 0 0; + padding: 0; + list-style: none; + font-size: 13.5px; + font-weight: 600; + color: var(--color-on-dark-accent); +} + +.website-call-me__card { + background: var(--color-panel); + border-radius: 18px; + padding: 28px 26px; + box-shadow: 0 40px 80px -40px rgba(0, 0, 0, 0.5); +} + +.website-call-me__card-title { + font-family: var(--font-display); + font-size: 17px; + font-weight: 700; + color: var(--color-deep-fir); + margin: 4px 0 18px; +} + +/* --- FAQ --- */ + +.website-faq { + padding: 100px clamp(40px, 10vw, 140px); + background: #ffffff; + border-top: 1px solid #e7ece1; +} + +.website-faq__grid { + display: grid; + grid-template-columns: 0.7fr 1.3fr; + gap: 56px; + align-items: start; +} + +.website-faq__title { + font-family: var(--font-display); + font-weight: 800; + font-size: 40px; + line-height: 1.08; + letter-spacing: -0.025em; + color: var(--color-deep-fir); + margin: 14px 0 0; +} + +.website-faq__aside { + font-size: 15.5px; + line-height: 1.55; + color: var(--color-body); + margin: 16px 0 0; +} + +.website-faq__aside--mobile { + display: none; + margin-top: 16px; + font-size: 14px; +} + +.website-faq__link { + font-weight: 650; + border-bottom: 1px solid #b7d69a; + color: var(--color-link); +} + +.website-faq__item { + padding: 20px 0; + border-bottom: 1px solid var(--color-border-soft); +} + +.website-faq__item--last { + border-bottom: none; +} + +.website-faq__question { + font-size: 16.5px; + font-weight: 700; + color: var(--color-deep-fir); + margin: 0; +} + +.website-faq__answer { + font-size: 14.5px; + line-height: 1.6; + color: var(--color-body); + margin: 9px 0 0; + max-width: 620px; +} + +/* --- Final CTA --- */ + +.website-final-cta { + padding: 64px clamp(40px, 10vw, 140px) 72px; + background: #ffffff; +} + +.website-final-cta__panel { + display: flex; + align-items: center; + justify-content: space-between; + gap: 32px; + background: var(--color-sulu); + border-radius: 40px; + padding: 32px 48px 32px 64px; + text-align: left; + overflow: hidden; +} + +.website-final-cta__copy { + min-width: 0; + flex: 1 1 auto; + align-self: auto; + padding-bottom: 0; +} + +.website-final-cta__title { + font-family: var(--font-display); + font-size: 48px; + line-height: 1.05; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--color-deep-fir); + margin: 0; + max-width: 640px; +} + +.website-final-cta__lead { + font-size: 16.5px; + line-height: 1.6; + color: #2f5a1b; + margin: 18px 0 0; + max-width: 460px; +} + +.website-final-cta__actions { + display: flex; + gap: 14px; + justify-content: flex-start; + margin-top: 34px; + flex-wrap: wrap; +} + +.website-final-cta__media { + flex: 0 0 auto; + display: flex; + align-items: center; + padding-top: 0; +} + +.website-final-cta__phone { + display: block; + width: min(360px, 34vw); + height: auto; + object-fit: contain; + object-position: center; + background: #ffffff; + padding: 10px; + border-radius: 28px; + box-shadow: + 0 0 0 1px rgba(10, 31, 10, 0.06), + 0 12px 28px -8px rgba(10, 31, 10, 0.28), + 0 28px 56px -20px rgba(10, 31, 10, 0.22); +} + +/* --- Content pages (About, Blog, Legal) --- */ + +.website-body--content { + background: #ffffff; +} + +.website-body--content .website-nav--simple { + padding-left: clamp(40px, 10vw, 140px); + padding-right: clamp(40px, 10vw, 140px); +} + +.website-content-page { + padding: 56px clamp(40px, 10vw, 140px) 72px; + max-width: none; +} + +.website-content-page__title { + position: relative; + display: inline-block; + font-family: var(--font-display); + font-size: clamp(40px, 6vw, 64px); + font-weight: 800; + letter-spacing: -0.015em; + word-spacing: 0.1em; + line-height: 1.05; + margin: 0; + color: var(--color-sulu); + -webkit-text-stroke: 0.75px var(--color-deep-fir); + paint-order: stroke fill; + text-shadow: 0 8px 20px rgba(10, 31, 10, 0.12); +} + +.website-content-page--centered-title .website-content-page__title { + display: block; + text-align: center; +} + +.website-content-page__effective-date { + margin: 12px 0 0; + font-size: 11pt; + font-style: italic; + line-height: 1.4; + color: var(--color-body); +} + +.website-content-page--centered-title .website-content-page__effective-date { + text-align: center; +} + +.website-content-page__body { + margin-top: 28px; + font-size: 16.5px; + line-height: 1.65; + color: var(--color-body); + max-width: 640px; +} + +.website-content-page__body p { + margin: 0 0 1em; +} + +.website-content-page__body p:last-child { + margin-bottom: 0; +} + +/* --- Brand strip (pre-footer) --- */ + +.website-brand-strip { + background: var(--color-sulu); + padding: 20px clamp(40px, 10vw, 140px); +} + +.website-brand-strip__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + flex-wrap: wrap; +} + +.website-body a.website-brand-strip__wordmark { + display: inline-flex; + align-items: center; + gap: 1px; + font-family: var(--font-display); + font-weight: 800; + font-size: 28px; + letter-spacing: -0.032em; + line-height: 1; + color: var(--color-deep-fir); +} + +.website-body a.website-brand-strip__wordmark:hover { + color: var(--color-deep-fir); +} + +.website-brand-strip__flower { + display: block; + margin: 0.12em -0.04em 0; + flex: none; +} + +.website-brand-strip__tagline { + margin: 0; + font-size: 17px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--color-deep-fir); +} + +/* --- Footer --- */ + +.website-footer { + background: var(--color-deep-fir-footer); + color: var(--color-on-dark-muted); + padding: 72px clamp(40px, 10vw, 140px) 36px; +} + +.website-footer__main { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 40px; + align-items: start; +} + +.website-footer__column-title { + font-size: 16.5px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-sulu); + margin: 0 0 20px; +} + +.website-footer__contact { + display: flex; + flex-direction: column; + gap: 20px; +} + +.website-footer__contact-item { + display: flex; + flex-direction: column; + gap: 6px; +} + +.website-footer__contact-label { + font-size: 15.5px; + font-weight: 500; + color: rgba(255, 255, 255, 0.72); +} + +.website-footer__contact-item a { + font-size: 15.5px; + font-weight: 400; + color: var(--color-on-dark-muted); +} + +.website-footer__contact-item a:hover { + color: #ffffff; +} + +.website-footer__nav { + display: flex; + flex-direction: column; + gap: 16px; + font-size: 15.5px; + font-weight: 400; +} + +.website-footer__nav a { + color: var(--color-on-dark-muted); +} + +.website-footer__nav a:hover { + color: #ffffff; +} + +.website-footer__industries { + display: flex; + flex-direction: column; + gap: 16px; + font-size: 15.5px; + font-weight: 400; + line-height: 1.35; + color: var(--color-on-dark-muted); +} + +.website-footer__address { + font-size: 15.5px; + font-weight: 400; + line-height: 1.75; + color: var(--color-on-dark-muted); +} + +.website-footer__legal { + margin-top: 72px; + padding-top: 28px; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + font-size: 15.5px; + color: var(--color-sulu); +} + +.website-footer__made { + display: inline-flex; + align-items: baseline; + gap: 0.35em; + line-height: 1.35; +} + +.website-footer__heart { + display: inline-block; + width: 1.05em; + height: 1.05em; + flex: none; + /* Drop slightly so the heart point sits under the text baseline */ + transform: translateY(0.22em); + background-color: var(--color-sulu); + -webkit-mask-image: url("../../img/website/footer/calliflowr-heart.svg"); + mask-image: url("../../img/website/footer/calliflowr-heart.svg"); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; +} + +/* --- Missed-call revenue calculator --- */ + +.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; +} + +.website-math { + padding: 96px clamp(40px, 6vw, 72px) 72px; + background: var(--color-deep-fir); + color: #ffffff; +} + +.website-math__grid { + display: grid; + grid-template-columns: 1.05fr 0.95fr; + gap: 80px; + align-items: center; +} + +.website-math__title { + max-width: 480px; + margin: 14px 0 0; + color: #ffffff; + font-family: var(--font-display); + font-size: 40px; + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.025em; +} + +.website-math__lead { + max-width: 500px; + margin: 20px 0 0; + color: var(--color-on-dark-muted); + font-size: 15.5px; + line-height: 1.7; +} + +.website-math__controls { + display: flex; + flex-direction: column; + gap: 30px; + margin-top: 42px; +} + +.website-math__control + .website-math__control { + padding-top: 30px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.website-math__control-heading { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 24px; + margin-bottom: 12px; +} + +.website-math__label { + display: block; + color: var(--color-on-dark-accent); + font-size: 12.5px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.website-math__hint { + margin: 3px 0 0; + color: #6f9450; + font-size: 12.5px; + font-weight: 550; +} + +.website-math__value { + color: var(--color-sulu); + font-family: var(--font-display); + font-size: 34px; + font-weight: 800; + line-height: 1; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; +} + +.website-math__range { + width: 100%; + accent-color: var(--color-sulu); + cursor: pointer; +} + +.website-math__scale { + display: flex; + justify-content: space-between; + margin-top: 6px; + color: #6f9450; + font-size: 11px; + font-weight: 650; +} + +.website-math__result { + display: flex; + align-items: center; + justify-content: center; +} + +.website-math-receipt { + position: relative; + width: min(380px, 100%); + transform: rotate(1.6deg); +} + +.website-math-receipt__tape { + position: absolute; + z-index: 2; + top: -15px; + left: 50%; + width: 124px; + height: 30px; + border-radius: 2px; + background: rgba(159, 232, 112, 0.85); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25); + transform: translateX(-50%) rotate(-2.5deg); +} + +.website-math-receipt__paper { + position: relative; + padding: 40px 36px 30px; + background: #fffef9; + color: var(--color-deep-fir); + box-shadow: + 0 60px 110px -46px rgba(0, 0, 0, 0.8), + 0 12px 28px -12px rgba(0, 0, 0, 0.4); +} + +.website-math-receipt__paper::after { + position: absolute; + right: 0; + bottom: -10px; + left: 0; + height: 11px; + content: ""; + background: + linear-gradient(-45deg, #fffef9 8px, transparent 0), + linear-gradient(45deg, #fffef9 8px, transparent 0); + background-size: 15px 15px; + filter: drop-shadow(0 6px 5px rgba(0, 0, 0, 0.3)); +} + +.website-math-receipt__header { + padding-bottom: 18px; + border-bottom: 1.5px dashed #d9d5c2; + text-align: center; +} + +.website-math-receipt__brand { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-family: var(--font-display); + font-size: 17px; + font-weight: 800; + letter-spacing: 0.08em; +} + +.website-math-receipt__header > p { + margin: 6px 0 0; + color: #a39f8a; + font-size: 10.5px; + font-weight: 650; + letter-spacing: 0.18em; +} + +.website-math-receipt__meta { + display: flex; + justify-content: space-between; + margin-top: 14px; + color: #bab6a2; + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.08em; +} + +.website-math-receipt__lines { + display: flex; + flex-direction: column; + gap: 15px; + margin: 0; + padding: 22px 0; + color: #3a4a38; + font-size: 14px; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.website-math-receipt__lines > div { + display: flex; + align-items: baseline; + gap: 9px; +} + +.website-math-receipt__lines > div::after { + order: 2; + flex: 1; + border-bottom: 2px dotted #d9d5c2; + content: ""; +} + +.website-math-receipt__lines dt { + order: 1; +} + +.website-math-receipt__lines dd { + order: 3; + margin: 0; + color: var(--color-deep-fir); + font-weight: 750; +} + +.website-math-receipt__total { + padding-top: 20px; + border-top: 1.5px dashed #d9d5c2; + text-align: center; +} + +.website-math-receipt__total p { + margin: 0; + color: #a39f8a; + font-size: 11px; + font-weight: 750; + letter-spacing: 0.14em; +} + +.website-math-receipt__total output { + display: inline-block; + margin-top: 10px; + padding: 3px 16px 6px; + border-radius: 10px; + background: var(--color-sulu); + color: var(--color-deep-fir); + font-family: var(--font-display); + font-size: 46px; + font-weight: 800; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; +} + +.website-math-receipt__total span { + display: block; + margin-top: 16px; + color: var(--color-body); + font-size: 13px; + font-weight: 650; + line-height: 1.55; +} + +.website-math-receipt__barcode { + width: 170px; + height: 32px; + margin: 22px auto 0; + opacity: 0.8; + background: repeating-linear-gradient( + 90deg, + var(--color-deep-fir) 0 2px, + transparent 2px 4px, + var(--color-deep-fir) 4px 7px, + transparent 7px 9px, + var(--color-deep-fir) 9px 10px, + transparent 10px 14px + ); +} + +.website-math-receipt__disclaimer { + margin: 12px 0 0; + color: #bab6a2; + font-size: 10px; + font-weight: 550; + line-height: 1.4; + text-align: center; +} + +.website-math__sources { + max-width: 880px; + margin: 64px auto 0; + color: rgba(201, 220, 182, 0.45); + font-size: 11.5px; + line-height: 1.75; + text-align: center; +} + +/* --- Honest comparison --- */ + +.website-comparison { + padding: 100px clamp(40px, 6vw, 72px) 120px; + background: #ffffff; +} + +.website-comparison__intro { + text-align: center; +} + +.website-comparison__title { + max-width: 640px; + margin: 14px auto 0; + color: var(--color-deep-fir); + font-family: var(--font-display); + font-size: 40px; + font-weight: 800; + line-height: 1.08; + letter-spacing: -0.025em; +} + +.website-comparison__lead { + max-width: 680px; + margin: 20px auto 0; + color: var(--color-body); + font-size: 15.5px; + line-height: 1.7; +} + +.website-comparison__scroll { + margin-top: 68px; + border: 1px solid var(--color-border-soft); + border-radius: 26px; + overflow-x: auto; +} + +.website-comparison__table { + width: 100%; + min-width: 1000px; + border-collapse: separate; + border-spacing: 0; + table-layout: fixed; +} + +.website-comparison__table th, +.website-comparison__table td { + padding: 18px 26px; + border-top: 1px solid #e7ece1; + color: var(--color-body); + font-size: 13.5px; + font-weight: 550; + line-height: 1.55; + text-align: left; + vertical-align: middle; +} + +.website-comparison__table thead th { + padding-top: 22px; + padding-bottom: 22px; + border-top: 0; + color: var(--color-body); + font-size: 14.5px; + font-weight: 700; +} + +.website-comparison__table th:first-child { + width: 22%; +} + +.website-comparison__table th:nth-child(2) { + width: 28%; +} + +.website-comparison__table th:nth-child(3), +.website-comparison__table th:nth-child(4) { + width: 25%; +} + +.website-comparison__table tbody th { + color: var(--color-deep-fir); + font-size: 14px; + font-weight: 700; +} + +.website-comparison__table .website-comparison__calliflowr { + border-color: rgba(255, 255, 255, 0.1); + background: var(--color-deep-fir); + color: #e4f0d8; + font-weight: 600; +} + +.website-comparison__table thead .website-comparison__calliflowr { + border-radius: 22px 22px 0 0; + color: var(--color-sulu); +} + +.website-comparison__table tbody tr:last-child .website-comparison__calliflowr { + border-radius: 0 0 22px 22px; +} + +.website-comparison__brand { + display: inline-flex; + align-items: center; + gap: 9px; + font-family: var(--font-display); + font-size: 17px; + font-weight: 800; + letter-spacing: -0.015em; +} + +.website-comparison__footnote { + max-width: 820px; + margin: 56px auto 0; + color: #a3ac9f; + font-size: 11.5px; + line-height: 1.75; + text-align: center; +} + diff --git a/static/css/website/tokens.css b/static/css/website/tokens.css new file mode 100644 index 0000000..b127a9f --- /dev/null +++ b/static/css/website/tokens.css @@ -0,0 +1,53 @@ +/* + * Calliflowr website design tokens (desktop v1). + * Brand colors: Deep Fir, Sulu. + */ + +:root { + /* Brand */ + --color-deep-fir: #0a1f0a; + --color-sulu: #9fe870; + --color-deep-fir-footer: #0a1f0a; + + /* Surfaces */ + --color-page: #f4f7f0; + --color-panel: #ffffff; + + /* Text */ + --color-ink: #0a1f0a; + --color-body: #5a6957; + --color-muted: #8a948a; + --color-link: #245a12; + --color-nav: #3a4a38; + + /* Accents */ + --color-eyebrow-bg: #eaf2e0; + --color-eyebrow-border: #d8e4c8; + --color-eyebrow-text: #245a12; + --color-live-dot: #5fbf3f; + --color-on-dark-muted: #c9dcb6; + --color-on-dark-accent: #8fb56a; + + /* Borders */ + --color-border: #d7ddce; + --color-border-soft: #e2e8da; + + /* Typography */ + --font-display: "Bricolage Grotesque", system-ui, sans-serif; + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif; + + /* Layout (desktop) */ + --site-gutter: 56px; + --section-gutter: 60px; + --mobile-max: 768px; + + /* Shape */ + --radius-sm: 10px; + --radius-md: 11px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-pill: 999px; + + /* Motion */ + --ease-hover: 0.15s ease; +} diff --git a/static/css/website/website.css b/static/css/website/website.css new file mode 100644 index 0000000..d965d6e --- /dev/null +++ b/static/css/website/website.css @@ -0,0 +1,7 @@ +@import url("tokens.css"); +@import url("base.css"); +@import url("components.css"); +@import url("sections.css"); +@import url("demo.css"); +@import url("login.css"); +@import url("responsive.css"); diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png index 0a5062a..21006cf 100644 Binary files a/static/favicon-16x16.png and b/static/favicon-16x16.png differ diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png index b510645..49fec9a 100644 Binary files a/static/favicon-32x32.png and b/static/favicon-32x32.png differ diff --git a/static/favicon-48x48.png b/static/favicon-48x48.png new file mode 100644 index 0000000..7f6954c Binary files /dev/null and b/static/favicon-48x48.png differ diff --git a/static/favicon.ico b/static/favicon.ico index 9616478..47ba03c 100644 Binary files a/static/favicon.ico and b/static/favicon.ico differ diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..6949744 --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/static/icon-192.png b/static/icon-192.png new file mode 100644 index 0000000..5bf1548 Binary files /dev/null and b/static/icon-192.png differ diff --git a/static/icon-512.png b/static/icon-512.png new file mode 100644 index 0000000..681d80c Binary files /dev/null and b/static/icon-512.png differ diff --git a/static/img/brand/apple-touch-icon.png b/static/img/brand/apple-touch-icon.png new file mode 100644 index 0000000..5eff7af Binary files /dev/null and b/static/img/brand/apple-touch-icon.png differ diff --git a/static/img/brand/blossom-dark.svg b/static/img/brand/blossom-dark.svg new file mode 100644 index 0000000..d23054e --- /dev/null +++ b/static/img/brand/blossom-dark.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/static/img/brand/blossom-lime-256.png b/static/img/brand/blossom-lime-256.png new file mode 100644 index 0000000..d60fc9a Binary files /dev/null and b/static/img/brand/blossom-lime-256.png differ diff --git a/static/img/brand/blossom-lime-512.png b/static/img/brand/blossom-lime-512.png new file mode 100644 index 0000000..4836359 Binary files /dev/null and b/static/img/brand/blossom-lime-512.png differ diff --git a/static/img/brand/blossom-lime.svg b/static/img/brand/blossom-lime.svg new file mode 100644 index 0000000..187cd50 --- /dev/null +++ b/static/img/brand/blossom-lime.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/static/img/brand/favicon-16.png b/static/img/brand/favicon-16.png new file mode 100644 index 0000000..21006cf Binary files /dev/null and b/static/img/brand/favicon-16.png differ diff --git a/static/img/brand/favicon-32.png b/static/img/brand/favicon-32.png new file mode 100644 index 0000000..49fec9a Binary files /dev/null and b/static/img/brand/favicon-32.png differ diff --git a/static/img/brand/favicon-48.png b/static/img/brand/favicon-48.png new file mode 100644 index 0000000..7f6954c Binary files /dev/null and b/static/img/brand/favicon-48.png differ diff --git a/static/img/brand/favicon.svg b/static/img/brand/favicon.svg new file mode 100644 index 0000000..6949744 --- /dev/null +++ b/static/img/brand/favicon.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/static/img/brand/icon-192.png b/static/img/brand/icon-192.png new file mode 100644 index 0000000..5bf1548 Binary files /dev/null and b/static/img/brand/icon-192.png differ diff --git a/static/img/brand/icon-512.png b/static/img/brand/icon-512.png new file mode 100644 index 0000000..681d80c Binary files /dev/null and b/static/img/brand/icon-512.png differ diff --git a/static/img/brand/icon-maskable.svg b/static/img/brand/icon-maskable.svg new file mode 100644 index 0000000..d1ab4b4 --- /dev/null +++ b/static/img/brand/icon-maskable.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/static/img/brand/wordmark-dark.svg b/static/img/brand/wordmark-dark.svg new file mode 100644 index 0000000..b10780a --- /dev/null +++ b/static/img/brand/wordmark-dark.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/static/img/website/footer/calliflowr-heart.svg b/static/img/website/footer/calliflowr-heart.svg new file mode 100644 index 0000000..d0fcfef --- /dev/null +++ b/static/img/website/footer/calliflowr-heart.svg @@ -0,0 +1,3 @@ + diff --git a/static/img/website/footer/footer-cta-short.png b/static/img/website/footer/footer-cta-short.png new file mode 100644 index 0000000..9de11db Binary files /dev/null and b/static/img/website/footer/footer-cta-short.png differ diff --git a/static/img/website/footer/footer-cta-shorter.png b/static/img/website/footer/footer-cta-shorter.png new file mode 100644 index 0000000..e6507a4 Binary files /dev/null and b/static/img/website/footer/footer-cta-shorter.png differ diff --git a/static/img/website/hero/calliflowr-cleaning.jpg b/static/img/website/hero/calliflowr-cleaning.jpg new file mode 100644 index 0000000..84bdc85 Binary files /dev/null and b/static/img/website/hero/calliflowr-cleaning.jpg differ diff --git a/static/img/website/hero/calliflowr-construction.jpg b/static/img/website/hero/calliflowr-construction.jpg new file mode 100644 index 0000000..d91586d Binary files /dev/null and b/static/img/website/hero/calliflowr-construction.jpg differ diff --git a/static/img/website/hero/calliflowr-electrical.jpg b/static/img/website/hero/calliflowr-electrical.jpg new file mode 100644 index 0000000..3a67764 Binary files /dev/null and b/static/img/website/hero/calliflowr-electrical.jpg differ diff --git a/static/img/website/hero/calliflowr-garage-door.jpg b/static/img/website/hero/calliflowr-garage-door.jpg new file mode 100644 index 0000000..c0fa0c5 Binary files /dev/null and b/static/img/website/hero/calliflowr-garage-door.jpg differ diff --git a/static/img/website/hero/calliflowr-hvac.jpg b/static/img/website/hero/calliflowr-hvac.jpg new file mode 100644 index 0000000..2874736 Binary files /dev/null and b/static/img/website/hero/calliflowr-hvac.jpg differ diff --git a/static/img/website/hero/calliflowr-pest-control.jpg b/static/img/website/hero/calliflowr-pest-control.jpg new file mode 100644 index 0000000..5a7e054 Binary files /dev/null and b/static/img/website/hero/calliflowr-pest-control.jpg differ diff --git a/static/img/website/hero/calliflowr-plumbing.jpg b/static/img/website/hero/calliflowr-plumbing.jpg new file mode 100644 index 0000000..a632043 Binary files /dev/null and b/static/img/website/hero/calliflowr-plumbing.jpg differ diff --git a/static/img/website/hero/calliflowr-roofing.jpg b/static/img/website/hero/calliflowr-roofing.jpg new file mode 100644 index 0000000..97c4b39 Binary files /dev/null and b/static/img/website/hero/calliflowr-roofing.jpg differ diff --git a/static/img/website/hero/calliflowr-solar.jpg b/static/img/website/hero/calliflowr-solar.jpg new file mode 100644 index 0000000..fb85877 Binary files /dev/null and b/static/img/website/hero/calliflowr-solar.jpg differ diff --git a/static/img/website/how-it-works/step-2.png b/static/img/website/how-it-works/step-2.png new file mode 100644 index 0000000..d4bfbcf Binary files /dev/null and b/static/img/website/how-it-works/step-2.png differ diff --git a/static/img/website/how-it-works/step-3.png b/static/img/website/how-it-works/step-3.png new file mode 100644 index 0000000..1ec9f55 Binary files /dev/null and b/static/img/website/how-it-works/step-3.png differ diff --git a/static/img/website/how-it-works/step-4.png b/static/img/website/how-it-works/step-4.png new file mode 100644 index 0000000..c246bcb Binary files /dev/null and b/static/img/website/how-it-works/step-4.png differ diff --git a/static/img/website/icons/trusted.png b/static/img/website/icons/trusted.png new file mode 100644 index 0000000..19005af Binary files /dev/null and b/static/img/website/icons/trusted.png differ diff --git a/static/img/website/screenshots/.gitkeep b/static/img/website/screenshots/.gitkeep new file mode 100644 index 0000000..34f365b --- /dev/null +++ b/static/img/website/screenshots/.gitkeep @@ -0,0 +1,3 @@ +# Placeholder until real dashboard screenshot is added. +# Set DASHBOARD_SCREENSHOT in utils/website/content.py to: +# img/website/screenshots/shot-dashboard.png diff --git a/static/js/website-calculator.js b/static/js/website-calculator.js new file mode 100644 index 0000000..7baffe5 --- /dev/null +++ b/static/js/website-calculator.js @@ -0,0 +1,37 @@ +(() => { + const calculator = document.querySelector("[data-revenue-calculator]"); + if (!calculator) return; + + const ticketInput = calculator.querySelector("[data-ticket]"); + const missedInput = calculator.querySelector("[data-missed]"); + if (!ticketInput || !missedInput) return; + + const currency = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }); + + const setText = (selector, value) => { + const element = calculator.querySelector(selector); + if (element) element.textContent = value; + }; + + const update = () => { + const ticket = Number(ticketInput.value); + const missed = Number(missedInput.value); + const weeklyLoss = ticket * missed; + const monthlyLoss = weeklyLoss * 4.33; + + setText("[data-ticket-label]", currency.format(ticket)); + setText("[data-missed-label]", String(missed)); + setText("[data-receipt-ticket]", currency.format(ticket)); + setText("[data-receipt-missed]", String(missed)); + setText("[data-weekly-loss]", currency.format(weeklyLoss)); + setText("[data-monthly-loss]", currency.format(monthlyLoss)); + }; + + ticketInput.addEventListener("input", update); + missedInput.addEventListener("input", update); + update(); +})(); diff --git a/static/js/website-call-me.js b/static/js/website-call-me.js new file mode 100644 index 0000000..b6921d9 --- /dev/null +++ b/static/js/website-call-me.js @@ -0,0 +1,141 @@ +(() => { + const phoneInput = document.querySelector("#call-me #phone"); + if (phoneInput instanceof HTMLInputElement) { + let previousDigitCount = 0; + + const onlyDigits = (raw) => { + let digits = String(raw || "").replace(/\D/g, ""); + if (digits.length === 11 && digits.startsWith("1")) { + digits = digits.slice(1); + } + return digits.slice(0, 10); + }; + + const formatPhone = (raw, { deleting = false } = {}) => { + const digits = onlyDigits(raw); + const area = digits.slice(0, 3); + const mid = digits.slice(3, 6); + const last = digits.slice(6, 10); + const n = digits.length; + + if (n === 0) return ""; + if (n < 3) return `(${area}`; + if (n === 3) return deleting ? `(${area}` : `(${area})`; + if (n < 6) return `(${area}) ${mid}`; + if (n === 6) return deleting ? `(${area}) ${mid}` : `(${area}) ${mid} -`; + return `(${area}) ${mid} - ${last}`; + }; + + const countDigitsBefore = (value, caret) => { + let count = 0; + const end = Math.min(caret, value.length); + for (let i = 0; i < end; i += 1) { + if (/\d/.test(value[i])) count += 1; + } + return count; + }; + + const caretFromDigitCount = (value, digitCount) => { + if (digitCount <= 0) return 0; + let seen = 0; + for (let i = 0; i < value.length; i += 1) { + if (/\d/.test(value[i])) { + seen += 1; + if (seen === digitCount) { + let pos = i + 1; + while (pos < value.length && !/\d/.test(value[pos])) pos += 1; + return pos; + } + } + } + return value.length; + }; + + const applyFormat = () => { + const previous = phoneInput.value; + const selectionStart = phoneInput.selectionStart; + const caret = selectionStart == null ? previous.length : selectionStart; + const typingAtEnd = caret >= previous.length; + const digits = onlyDigits(previous); + const digitCount = digits.length; + const deleting = digitCount < previousDigitCount; + const digitsBefore = countDigitsBefore(previous, caret); + const next = formatPhone(previous, { deleting }); + + previousDigitCount = digitCount; + + if (next !== previous) { + phoneInput.value = next; + } + + const nextCaret = typingAtEnd + ? next.length + : caretFromDigitCount(next, Math.min(digitsBefore, digitCount)); + + try { + phoneInput.setSelectionRange(nextCaret, nextCaret); + } catch (_) { + // Ignore browsers that reject setSelectionRange on tel inputs. + } + }; + + phoneInput.addEventListener("keydown", (event) => { + if (event.key !== "Backspace") return; + if (phoneInput.selectionStart !== phoneInput.selectionEnd) return; + + const caret = phoneInput.selectionStart ?? 0; + if (caret <= 0) return; + + const charBefore = phoneInput.value[caret - 1]; + if (/\d/.test(charBefore)) return; + + event.preventDefault(); + const digits = onlyDigits(phoneInput.value); + if (digits.length === 0) { + phoneInput.value = ""; + previousDigitCount = 0; + return; + } + + const trimmed = digits.slice(0, -1); + previousDigitCount = digits.length; + phoneInput.value = formatPhone(trimmed, { deleting: true }); + previousDigitCount = trimmed.length; + const pos = phoneInput.value.length; + phoneInput.setSelectionRange(pos, pos); + }); + + phoneInput.addEventListener("input", applyFormat); + phoneInput.addEventListener("blur", () => { + previousDigitCount = onlyDigits(phoneInput.value).length; + phoneInput.value = formatPhone(phoneInput.value, { deleting: false }); + }); + + if (phoneInput.value) { + previousDigitCount = onlyDigits(phoneInput.value).length; + applyFormat(); + } + } + + const crmSelect = document.querySelector("#call-me #crm"); + const crmOtherField = document.querySelector("#call-me #crm-other-field"); + const crmOtherInput = document.querySelector("#call-me #crm_other"); + if ( + crmSelect instanceof HTMLSelectElement && + crmOtherField instanceof HTMLElement && + crmOtherInput instanceof HTMLInputElement + ) { + const syncCrmOther = () => { + const showOther = crmSelect.value === "other"; + crmOtherField.hidden = !showOther; + crmOtherField.classList.toggle("is-hidden", !showOther); + crmOtherInput.required = showOther; + if (!showOther) { + crmOtherInput.value = ""; + } + }; + + crmSelect.addEventListener("change", syncCrmOther); + syncCrmOther(); + } +})(); diff --git a/static/js/website-hero.js b/static/js/website-hero.js new file mode 100644 index 0000000..cb1609a --- /dev/null +++ b/static/js/website-hero.js @@ -0,0 +1,163 @@ +(() => { + const root = document.querySelector("[data-hero-showcase]"); + if (!root) return; + + const cards = Array.from(root.querySelectorAll(".website-hero__card")); + const genreStrip = root.querySelector(".website-hero__genres"); + if (!cards.length || !genreStrip) return; + + const originals = Array.from(genreStrip.querySelectorAll(".website-hero__genre")); + if (!originals.length) return; + + const count = originals.length; + + // Three copies: work in the middle set so both sides always have neighbors. + for (let copy = 0; copy < 2; copy += 1) { + originals.forEach((genre) => { + const clone = genre.cloneNode(true); + clone.setAttribute("aria-hidden", "true"); + clone.tabIndex = -1; + genreStrip.appendChild(clone); + }); + } + + const genres = Array.from(genreStrip.querySelectorAll(".website-hero__genre")); + + let index = 0; + // Start on the middle copy so neighbors exist on both sides. + let visualIndex = count; + let timer = null; + let transitioning = false; + const INTERVAL_MS = 2600; + + function loopWidth() { + return genres[count].offsetLeft - genres[0].offsetLeft; + } + + function centerOn(buttonIndex, behavior = "smooth") { + const button = genres[buttonIndex]; + if (!button) return; + + const stripRect = genreStrip.getBoundingClientRect(); + const genreRect = button.getBoundingClientRect(); + const target = + genreStrip.scrollLeft + + (genreRect.left - stripRect.left) - + (stripRect.width - genreRect.width) / 2; + + genreStrip.scrollTo({ left: target, behavior }); + } + + function syncActiveClasses() { + cards.forEach((card, i) => { + const active = i === index; + card.classList.toggle("is-active", active); + card.setAttribute("aria-hidden", active ? "false" : "true"); + }); + + genres.forEach((genre) => { + const active = Number(genre.dataset.slideIndex) === index; + genre.classList.toggle("is-active", active); + if (genre.getAttribute("aria-hidden") !== "true") { + genre.setAttribute("aria-selected", active ? "true" : "false"); + } + }); + } + + /** + * Instantly shift scroll by whole loops so visualIndex stays in the middle + * copy [count, 2*count). Content looks identical; no animation. + */ + function snapToMiddleCopy() { + const width = loopWidth(); + if (!width) return; + + while (visualIndex >= 2 * count) { + genreStrip.scrollLeft -= width; + visualIndex -= count; + } + while (visualIndex < count) { + genreStrip.scrollLeft += width; + visualIndex += count; + } + } + + /** + * Before a forward step, if the next pill would be past the middle copy, + * jump back one loop first so we always smooth-scroll onto a real node. + */ + function prepareForwardStep() { + if (visualIndex < 2 * count - 1) return; + const width = loopWidth(); + if (!width) return; + genreStrip.scrollLeft -= width; + visualIndex -= count; + } + + function setActive(nextIndex, targetVisual, behavior = "smooth") { + index = ((nextIndex % count) + count) % count; + visualIndex = targetVisual; + syncActiveClasses(); + centerOn(visualIndex, behavior); + } + + function next() { + if (transitioning) return; + transitioning = true; + + prepareForwardStep(); + setActive(index + 1, visualIndex + 1, "smooth"); + + window.setTimeout(() => { + snapToMiddleCopy(); + transitioning = false; + }, 500); + } + + function start() { + stop(); + timer = window.setInterval(next, INTERVAL_MS); + } + + function stop() { + if (timer !== null) { + window.clearInterval(timer); + timer = null; + } + } + + genreStrip.addEventListener("click", (event) => { + const button = event.target.closest(".website-hero__genre"); + if (!button || !genreStrip.contains(button)) return; + + const nextIndex = Number(button.dataset.slideIndex || 0); + let targetVisual = genres.indexOf(button); + + // Prefer the forward instance when the strip is moving ahead. + if (targetVisual <= visualIndex) { + const forwardTwin = targetVisual + count; + if (forwardTwin < genres.length && forwardTwin > visualIndex) { + targetVisual = forwardTwin; + } + } + + // Keep selection in/near the middle copy when possible. + if (targetVisual < count) { + targetVisual += count; + } else if (targetVisual >= 2 * count) { + targetVisual -= count; + genreStrip.scrollLeft -= loopWidth(); + } + + transitioning = false; + setActive(nextIndex, targetVisual); + start(); + }); + + root.addEventListener("mouseenter", stop); + root.addEventListener("mouseleave", start); + + syncActiveClasses(); + centerOn(visualIndex, "auto"); + start(); +})(); diff --git a/static/site.webmanifest b/static/site.webmanifest index 65c2cbd..aec02ed 100644 --- a/static/site.webmanifest +++ b/static/site.webmanifest @@ -1,14 +1,21 @@ { - "name": "", - "short_name": "", - "icons": [ - { - "src": "/static/android-chrome-144x144.png", - "sizes": "144x144", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "Calliflowr", + "short_name": "Calliflowr", + "icons": [ + { + "src": "/static/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/static/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "theme_color": "#0a1f0a", + "background_color": "#f4f7f0", + "display": "standalone" } diff --git a/templates/account/calliflowr_auth_base.html b/templates/account/calliflowr_auth_base.html new file mode 100644 index 0000000..52098e0 --- /dev/null +++ b/templates/account/calliflowr_auth_base.html @@ -0,0 +1,43 @@ + + +
+ + ++ Enter the email for your Calliflowr account and we'll send a link to set a new password. +
-{% block auth_content %} -Remember your password? Login here
- {% else %} -If an account exists with this email, a password reset link will be sent. Note that you can request a password reset only once an hour. If you have not received an email, please check your spam folder or try again later.
- {% endif %} ++ Remember your password? + Back to sign in +
+ {% else %} ++ If an account exists with that email, a password reset link is on the way. + You can request another reset once an hour. Haven't seen it? Check spam, then try again later. +
+ + {% if request.state.flash %} ++ This invitation link has expired. Ask your organization administrator to send a new invitation, then use the link in the latest email. +
+ {% else %} ++ This invitation link is no longer valid. If you were invited again recently, use the link from the most recent invitation email. +
+ {% endif %} + Return to dashboard + {% else %} ++ Enter the email and password for your Calliflowr account. +
- {% if show_warning_only %} - - {% else %} - + + + - - +Don't have an account? Register here
- {% endif %} + + + {% endif %} +