feat(phasefinal-web): corporate site stack on ana-docker

Single static page (nginx) fronted by Traefik at www.phasefinal.com, built
from the design brief. Site markup/CSS checked in verbatim from the design
session; fonts self-hosted (SIL OFL) with the @font-face block uncommented,
which every fresh export re-comments.

Routed via a Traefik file-provider config rather than the container labels:
the docker provider on ana-docker was not registering newly-created
containers, so the file router avoids restarting shared ingress. Labels are
retained in compose so the file can be dropped once that is fixed.
This commit is contained in:
2026-08-29 22:25:20 -07:00
parent c488eadc31
commit 7ffbee6f09
16 changed files with 2196 additions and 624 deletions
+1652 -624
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
# phasefinal-web
The Phase Final, Inc. corporate site — a single static page served by nginx on
**ana-docker**, fronted by Traefik at `www.phasefinal.com`.
Built from the design brief at `docs/design/` (boothed 2026-08-29); the markup
and CSS came from a Claude Design session and are checked in verbatim under
`conf/site/` apart from the two post-export steps below.
## Two steps every fresh export needs
The design tool ships the site with **no font binaries** and the `@font-face`
block **commented out**, deliberately, to honour the brief's zero-external-
requests rule. Both must be undone on import:
1. Drop the four woff2 files into `conf/site/fonts/`
(Space Grotesk variable, IBM Plex Sans variable, IBM Plex Mono 400 + 600 —
all SIL OFL, self-hosted, no CDN).
2. Uncomment the four `@font-face` rules at the top of `conf/site/style.css`.
Without these the page silently falls back to system fonts and looks wrong
rather than broken, which is the failure mode you won't notice.
## Routing
`conf/traefik-dynamic-phasefinal-web.yml` deploys to
**`/opt/docker/conf/traefik-ana/config/phasefinal-web.yml`** — not into this
stack's own conf dir. It exists because Traefik's docker provider on ana-docker
was not registering newly-created containers on 2026-08-29; the compose file
still carries the correct `traefik.*` labels, so once that is fixed the file
router can be deleted. See the header comment in that file.
## Deploy
```bash
scripts/deploy-stack.sh ana-docker phasefinal-web
ssh infra-ops@10.250.50.70 'cd /opt/docker/compose/phasefinal-web && docker compose up -d'
```
## DNS
`www.phasefinal.com` → A `38.120.12.44` (the Anaheim public IP, forwarded to
ana-docker's Traefik). The apex `phasefinal.com` has **no** record — the site is
www-only by operator decision. Cloudflare-proxied is the intended end state, for
edge caching and the automatic email-address obfuscation the contact block
relies on.
## Content constraints
The copy is governed by hard legal and voice constraints — client naming rules,
the never-"military"/"weapons" phrasing rule, no named individuals. **Read the
design brief before editing any text.** Edits are made surgically in
`conf/site/index.html`; there is no CMS and there will not be one.
+26
View File
@@ -0,0 +1,26 @@
services:
web:
image: nginx:alpine
container_name: phasefinal-web
restart: unless-stopped
volumes:
- /opt/docker/conf/phasefinal-web/site:/usr/share/nginx/html:ro
- /opt/docker/conf/phasefinal-web/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- tnet
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
labels:
- traefik.enable=true
- traefik.http.routers.phasefinal-web.rule=Host(`www.phasefinal.com`)
- traefik.http.routers.phasefinal-web.tls=true
- traefik.http.routers.phasefinal-web.tls.certresolver=anaprod
- traefik.http.services.phasefinal-web.loadbalancer.server.port=80
networks:
tnet:
name: traefik-net
external: true
+18
View File
@@ -0,0 +1,18 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Static site; long cache on immutable assets, short on the document so
# a dictated edit goes live on the next deploy without a purge.
location = / { add_header Cache-Control "public, max-age=300"; try_files /index.html =404; }
location ~* \.(woff2|svg|png|ico)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }
location ~* \.css$ { add_header Cache-Control "public, max-age=3600"; }
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy no-referrer always;
location / { try_files $uri $uri/ =404; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,19 @@
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" viewBox="1000 1820 6280 1800" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
</defs>
<g id="Foreground">
<metadata id="CorelCorpID_0Corel-Layer"></metadata>
<polygon fill="#ffffff" points="3750,3050 3655,3050 3655,2854 3418,2854 3418,3050 3322,3050 3322,2582 3418,2582 3418,2772 3655,2772 3655,2582 3750,2582 "></polygon>
<path fill="#ffffff" d="M4282 3050l-106 0 -43 -112 -205 0 -41 112 -100 0 195 -468 102 0 198 468zm-181 -194l0 0 -71 -186 -70 186 141 0z"></path>
<path fill="#ffffff" d="M4732 2911c0,40 -9,72 -26,93 -25,31 -70,46 -136,46l-259 0 0 -82 244 0c25,0 44,-3 56,-10 14,-8 21,-22 21,-43 0,-20 -5,-35 -17,-44 -11,-9 -29,-13 -54,-13l-115 0c-51,0 -89,-10 -114,-31 -27,-22 -40,-57 -40,-104 0,-49 13,-85 39,-108 26,-22 67,-33 122,-33l256 0 0 82 -240 0c-52,0 -78,21 -78,63 0,17 6,30 18,39 11,9 27,13 46,13l129 0c46,0 79,6 99,18 33,19 49,57 49,114z"></path>
<path fill="#ffffff" d="M5158 3050l-164 0c-67,0 -119,-17 -156,-52 -40,-38 -60,-96 -60,-173 0,-91 19,-155 58,-193 34,-34 88,-50 161,-50l161 0 0 79 -160 0c-35,0 -62,9 -80,27 -19,19 -29,48 -29,86l269 0 0 79 -271 0c0,44 11,75 31,94 18,16 44,24 80,24l160 0 0 79z"></path>
<path fill="#90c73e" d="M5638 2857l-278 0 0 193 -97 0 0 -215c0,-59 2,-101 7,-124 9,-42 28,-73 58,-93 36,-24 90,-36 162,-36l148 0 0 79 -156 0c-43,0 -75,11 -95,33 -18,19 -27,48 -27,84l278 0 0 79z"></path>
<polygon fill="#90c73e" points="5806,3050 5706,3050 5706,2582 5806,2582 "></polygon>
<polygon fill="#90c73e" points="6285,3050 6174,3050 5973,2694 5973,3050 5878,3050 5878,2582 6001,2582 6193,2920 6193,2582 6285,2582 "></polygon>
<path fill="#90c73e" d="M7144 3050l-139 0c-66,0 -113,-16 -141,-47 -25,-29 -38,-77 -38,-142l0 -279 95 0 0 244c0,5 0,10 0,14 0,5 0,9 0,12 0,34 5,58 15,74 16,23 47,35 92,35l116 0 0 89z"></path>
<path fill="#ffffff" d="M2656 3296c0,101 -83,183 -184,183l-1295 0 0 -1098c0,-101 82,-205 184,-205l1111 0c101,0 184,82 184,183l0 937zm-201 -542c0,-208 -169,-376 -377,-376 -2,0 -38,0 -38,0l-62 213c52,0 100,0 101,0 90,0 163,73 163,163 0,90 -73,164 -163,164 -1,0 -141,0 -193,0l-62 212c0,0 252,1 254,1 208,0 377,-169 377,-377zm-848 0c0,-90 73,-163 163,-163 0,0 73,0 125,0l62 -213c0,0 -184,0 -187,0 -207,0 -375,168 -376,375l0 0 0 555 212 0 0 -178 133 0 62 -211 -195 0 0 -165 1 0z"></path>
<path fill="#ffffff" d="M3236 2618c-31,-24 -82,-36 -155,-36l-212 0 0 82 96 0 0 0 114 0c33,0 57,5 73,15 15,10 23,29 23,55 0,47 -31,70 -91,70l-119 0 0 -95 -96 0 0 341 96 0 0 -164 120 0c2,0 4,1 6,1 4,0 8,0 12,0 43,0 79,-8 108,-23 46,-25 70,-68 70,-130 0,-53 -15,-92 -45,-116z"></path>
<polygon fill="#90c73e" points="6616,2582 6515,2582 6320,3050 6420,3050 6563,2670 6634,2856 6531,2856 6501,2938 6665,2938 6709,3050 6814,3050 "></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,19 @@
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" viewBox="1000 1820 6280 1800" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
</defs>
<g id="Foreground">
<metadata id="CorelCorpID_0Corel-Layer"></metadata>
<polygon fill="#ffffff" points="3750,3050 3655,3050 3655,2854 3418,2854 3418,3050 3322,3050 3322,2582 3418,2582 3418,2772 3655,2772 3655,2582 3750,2582 "></polygon>
<path fill="#ffffff" d="M4282 3050l-106 0 -43 -112 -205 0 -41 112 -100 0 195 -468 102 0 198 468zm-181 -194l0 0 -71 -186 -70 186 141 0z"></path>
<path fill="#ffffff" d="M4732 2911c0,40 -9,72 -26,93 -25,31 -70,46 -136,46l-259 0 0 -82 244 0c25,0 44,-3 56,-10 14,-8 21,-22 21,-43 0,-20 -5,-35 -17,-44 -11,-9 -29,-13 -54,-13l-115 0c-51,0 -89,-10 -114,-31 -27,-22 -40,-57 -40,-104 0,-49 13,-85 39,-108 26,-22 67,-33 122,-33l256 0 0 82 -240 0c-52,0 -78,21 -78,63 0,17 6,30 18,39 11,9 27,13 46,13l129 0c46,0 79,6 99,18 33,19 49,57 49,114z"></path>
<path fill="#ffffff" d="M5158 3050l-164 0c-67,0 -119,-17 -156,-52 -40,-38 -60,-96 -60,-173 0,-91 19,-155 58,-193 34,-34 88,-50 161,-50l161 0 0 79 -160 0c-35,0 -62,9 -80,27 -19,19 -29,48 -29,86l269 0 0 79 -271 0c0,44 11,75 31,94 18,16 44,24 80,24l160 0 0 79z"></path>
<path fill="#ffffff" d="M5638 2857l-278 0 0 193 -97 0 0 -215c0,-59 2,-101 7,-124 9,-42 28,-73 58,-93 36,-24 90,-36 162,-36l148 0 0 79 -156 0c-43,0 -75,11 -95,33 -18,19 -27,48 -27,84l278 0 0 79z"></path>
<polygon fill="#ffffff" points="5806,3050 5706,3050 5706,2582 5806,2582 "></polygon>
<polygon fill="#ffffff" points="6285,3050 6174,3050 5973,2694 5973,3050 5878,3050 5878,2582 6001,2582 6193,2920 6193,2582 6285,2582 "></polygon>
<path fill="#ffffff" d="M7144 3050l-139 0c-66,0 -113,-16 -141,-47 -25,-29 -38,-77 -38,-142l0 -279 95 0 0 244c0,5 0,10 0,14 0,5 0,9 0,12 0,34 5,58 15,74 16,23 47,35 92,35l116 0 0 89z"></path>
<path fill="#ffffff" d="M2656 3296c0,101 -83,183 -184,183l-1295 0 0 -1098c0,-101 82,-205 184,-205l1111 0c101,0 184,82 184,183l0 937zm-201 -542c0,-208 -169,-376 -377,-376 -2,0 -38,0 -38,0l-62 213c52,0 100,0 101,0 90,0 163,73 163,163 0,90 -73,164 -163,164 -1,0 -141,0 -193,0l-62 212c0,0 252,1 254,1 208,0 377,-169 377,-377zm-848 0c0,-90 73,-163 163,-163 0,0 73,0 125,0l62 -213c0,0 -184,0 -187,0 -207,0 -375,168 -376,375l0 0 0 555 212 0 0 -178 133 0 62 -211 -195 0 0 -165 1 0z"></path>
<path fill="#ffffff" d="M3236 2618c-31,-24 -82,-36 -155,-36l-212 0 0 82 96 0 0 0 114 0c33,0 57,5 73,15 15,10 23,29 23,55 0,47 -31,70 -91,70l-119 0 0 -95 -96 0 0 341 96 0 0 -164 120 0c2,0 4,1 6,1 4,0 8,0 12,0 43,0 79,-8 108,-23 46,-25 70,-68 70,-130 0,-53 -15,-92 -45,-116z"></path>
<polygon fill="#ffffff" points="6616,2582 6515,2582 6320,3050 6420,3050 6563,2670 6634,2856 6531,2856 6501,2938 6665,2938 6709,3050 6814,3050 "></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,19 @@
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" viewBox="1000 1820 6280 1800" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
</defs>
<g id="Foreground">
<metadata id="CorelCorpID_0Corel-Layer"></metadata>
<polygon fill="#14191f" points="3750,3050 3655,3050 3655,2854 3418,2854 3418,3050 3322,3050 3322,2582 3418,2582 3418,2772 3655,2772 3655,2582 3750,2582 "></polygon>
<path fill="#14191f" d="M4282 3050l-106 0 -43 -112 -205 0 -41 112 -100 0 195 -468 102 0 198 468zm-181 -194l0 0 -71 -186 -70 186 141 0z"></path>
<path fill="#14191f" d="M4732 2911c0,40 -9,72 -26,93 -25,31 -70,46 -136,46l-259 0 0 -82 244 0c25,0 44,-3 56,-10 14,-8 21,-22 21,-43 0,-20 -5,-35 -17,-44 -11,-9 -29,-13 -54,-13l-115 0c-51,0 -89,-10 -114,-31 -27,-22 -40,-57 -40,-104 0,-49 13,-85 39,-108 26,-22 67,-33 122,-33l256 0 0 82 -240 0c-52,0 -78,21 -78,63 0,17 6,30 18,39 11,9 27,13 46,13l129 0c46,0 79,6 99,18 33,19 49,57 49,114z"></path>
<path fill="#14191f" d="M5158 3050l-164 0c-67,0 -119,-17 -156,-52 -40,-38 -60,-96 -60,-173 0,-91 19,-155 58,-193 34,-34 88,-50 161,-50l161 0 0 79 -160 0c-35,0 -62,9 -80,27 -19,19 -29,48 -29,86l269 0 0 79 -271 0c0,44 11,75 31,94 18,16 44,24 80,24l160 0 0 79z"></path>
<path fill="#90c73e" d="M5638 2857l-278 0 0 193 -97 0 0 -215c0,-59 2,-101 7,-124 9,-42 28,-73 58,-93 36,-24 90,-36 162,-36l148 0 0 79 -156 0c-43,0 -75,11 -95,33 -18,19 -27,48 -27,84l278 0 0 79z"></path>
<polygon fill="#90c73e" points="5806,3050 5706,3050 5706,2582 5806,2582 "></polygon>
<polygon fill="#90c73e" points="6285,3050 6174,3050 5973,2694 5973,3050 5878,3050 5878,2582 6001,2582 6193,2920 6193,2582 6285,2582 "></polygon>
<path fill="#90c73e" d="M7144 3050l-139 0c-66,0 -113,-16 -141,-47 -25,-29 -38,-77 -38,-142l0 -279 95 0 0 244c0,5 0,10 0,14 0,5 0,9 0,12 0,34 5,58 15,74 16,23 47,35 92,35l116 0 0 89z"></path>
<path fill="#14191f" d="M2656 3296c0,101 -83,183 -184,183l-1295 0 0 -1098c0,-101 82,-205 184,-205l1111 0c101,0 184,82 184,183l0 937zm-201 -542c0,-208 -169,-376 -377,-376 -2,0 -38,0 -38,0l-62 213c52,0 100,0 101,0 90,0 163,73 163,163 0,90 -73,164 -163,164 -1,0 -141,0 -193,0l-62 212c0,0 252,1 254,1 208,0 377,-169 377,-377zm-848 0c0,-90 73,-163 163,-163 0,0 73,0 125,0l62 -213c0,0 -184,0 -187,0 -207,0 -375,168 -376,375l0 0 0 555 212 0 0 -178 133 0 62 -211 -195 0 0 -165 1 0z"></path>
<path fill="#14191f" d="M3236 2618c-31,-24 -82,-36 -155,-36l-212 0 0 82 96 0 0 0 114 0c33,0 57,5 73,15 15,10 23,29 23,55 0,47 -31,70 -91,70l-119 0 0 -95 -96 0 0 341 96 0 0 -164 120 0c2,0 4,1 6,1 4,0 8,0 12,0 43,0 79,-8 108,-23 46,-25 70,-68 70,-130 0,-53 -15,-92 -45,-116z"></path>
<polygon fill="#90c73e" points="6616,2582 6515,2582 6320,3050 6420,3050 6563,2670 6634,2856 6531,2856 6501,2938 6665,2938 6709,3050 6814,3050 "></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#90C73E"></rect></svg>

After

Width:  |  Height:  |  Size: 124 B

+107
View File
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Phase Final, Inc. — Systems Engineering Consultancy</title>
<meta name="description" content="Phase Final, Inc. is a systems engineering consultancy offering systems engineering, infrastructure hosting, and AI services, with a track record in hardware, firmware, and software engineering.">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header id="hero">
<div class="hero-inner">
<img class="wordmark" src="assets/pfi-wordmark-duotone-dark.svg" alt="Phase Final, Inc.">
<div class="hero-copy">
<p class="lede">A systems engineering consultancy.</p>
<p class="sublede">Phase Final, Inc. has a long track record in hardware, firmware, and software engineering across an unusually wide range of industries: consumer electronics, financial services, health services, government, film and video game production, and industrial automation.</p>
</div>
<p class="spec-bar">
<span>Systems engineering</span>
<span>Infrastructure hosting</span>
<span>AI services</span>
<span class="spec-accent">Fixed-bid or retainer</span>
</p>
</div>
</header>
<main>
<section id="services">
<h2><span class="num">01</span>What we do</h2>
<div class="section-body">
<ul class="service-list">
<li>
<h3>Systems Engineering</h3>
<p>Design and integration of complete systems, hardware and software. Requirements, architecture, implementation, and verification are treated as one problem with one owner.</p>
</li>
<li>
<h3>Infrastructure Hosting</h3>
<p>Hosting and operation of client infrastructure. Systems the firm builds can run on infrastructure the firm operates, under the same accountability.</p>
</li>
<li>
<h3>AI Services &amp; Engineering</h3>
<p>Engineering and operation of AI systems. Model-backed capability is built, integrated, and run as production software, held to the same standard as everything else the firm ships.</p>
</li>
</ul>
</div>
</section>
<section id="engagement">
<h2><span class="num">02</span>How we engage</h2>
<div class="section-body">
<p>Engagements are fixed-bid or retainer. Fixed-bid where the scope can be specified: a defined deliverable at a defined price. Retainer where the work is continuous: operation, maintenance, and engineering capacity over time.</p>
<p class="referral-note">Most new work arrives by referral.</p>
</div>
</section>
<section id="track-record">
<h2><span class="num">03</span>Where we've done it</h2>
<div class="section-body">
<dl class="record-list">
<div class="record">
<dt><span class="idx">A</span> High-end consumer audio and electronics</dt>
<dd>Firmware design, software tooling, and R&amp;D engineering for <strong>Audeze</strong> and <strong>Sony</strong>.</dd>
</div>
<div class="record">
<dt><span class="idx">B</span> Portable illumination</dt>
<dd>Illumination and lighting technology for <strong>SureFire</strong>, including flashlights. The firm holds patents in lighting technology.</dd>
</div>
<div class="record">
<dt><span class="idx">C</span> Financial services</dt>
<dd>Mortgage lending underwriting engines for <strong>Fannie Mae</strong> and <strong>Freddie Mac</strong>. Credit card processing and gift card systems.</dd>
</div>
<div class="record">
<dt><span class="idx">D</span> Health services and medical industry</dt>
<dd>Document automation and billing systems.</dd>
</div>
<div class="record">
<dt><span class="idx">E</span> Government and DoD</dt>
<dd>Hardware and firmware, as a subcontractor to prime contractors.</dd>
</div>
<div class="record">
<dt><span class="idx">F</span> Film and video game production</dt>
<dd>Automated asset processing, rapid prototyping, and workflow automation.</dd>
</div>
<div class="record">
<dt><span class="idx">G</span> Industrial automation</dt>
<dd>CNC systems automation, additive manufacturing, and robotics automation.</dd>
</div>
</dl>
</div>
</section>
<section id="contact">
<h2><span class="num">04</span>Contact</h2>
<p class="contact-address"><a href="mailto:inquiry@phasefinal.com">inquiry@phasefinal.com</a></p>
<p class="contact-note">Tell us the spec and the deadline. We'll tell you what it takes to reach the final phase.</p>
</section>
<footer>
<span>Phase Final, Inc.</span>
</footer>
</main>
</body>
</html>
+255
View File
@@ -0,0 +1,255 @@
/* Phase Final, Inc. — single-page site
Brand: Volt #90C73E over cool slate neutrals, per Phase Final tokens.
Zero external requests; no JavaScript.
FONTS: the brand faces are Space Grotesk (display), IBM Plex Sans (body),
IBM Plex Mono (labels). To keep this zip free of external requests they are
referenced by local name only, with system fallbacks. To ship the exact
faces, place woff2 files in fonts/ and uncomment the @font-face blocks. */
@font-face { font-family: "Space Grotesk"; src: url("fonts/SpaceGrotesk-Var.woff2") format("woff2"); font-weight: 300 700; font-display: swap; }
@font-face { font-family: "IBM Plex Sans"; src: url("fonts/IBMPlexSans-Var.woff2") format("woff2"); font-weight: 100 700; font-display: swap; }
@font-face { font-family: "IBM Plex Mono"; src: url("fonts/IBMPlexMono-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "IBM Plex Mono"; src: url("fonts/IBMPlexMono-SemiBold.woff2") format("woff2"); font-weight: 600; font-display: swap; }
:root {
--ink: #0f1318;
--ink-2: #181d22;
--text: #0f1318;
--text-secondary: #4d5763;
--text-tertiary: #6b7682;
--paper: #f6f8f9;
--border: #c2cad2;
--border-subtle: #dde2e7;
--accent: #90c73e;
--accent-text: #588021; /* AA on light */
--volt-200: #d0e8a0;
--volt-300: #b8db72;
--volt-400: #a3d052;
--volt-500: #90c73e;
--slate-300: #c2cad2;
--slate-400: #97a2ad;
--slate-700: #394149;
--slate-800: #262c33;
--display: "Space Grotesk", "IBM Plex Sans", -apple-system, "Segoe UI", Helvetica, sans-serif;
--sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
--mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--text: #f6f8f9;
--text-secondary: #c2cad2;
--text-tertiary: #97a2ad;
--paper: #0f1318;
--border: #394149;
--border-subtle: #262c33;
--accent-text: #b8db72;
--ink: #0a0d11;
--ink-2: #181d22;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--text);
font-family: var(--sans);
font-size: 16.5px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
}
::selection { background: var(--volt-200); color: #0f1318; }
a { color: var(--accent-text); text-decoration: underline; text-underline-offset: 3px; }
p { margin: 0; text-wrap: pretty; }
/* ---- Hero: ink console surface with engineering grid ---- */
#hero {
background: var(--ink);
color: #f6f8f9;
position: relative;
overflow: hidden;
background-image:
linear-gradient(rgba(246,248,249,0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(246,248,249,0.05) 1px, transparent 1px);
background-size: 48px 48px;
border-bottom: 3px solid var(--volt-500);
}
.hero-inner {
max-width: 920px;
margin: 0 auto;
padding: clamp(56px, 9vw, 104px) 28px clamp(56px, 8vw, 96px);
display: flex;
flex-direction: column;
gap: clamp(40px, 6vw, 64px);
}
.wordmark { height: clamp(48px, 7.5vw, 72px); width: auto; display: block; align-self: flex-start; }
.hero-copy { max-width: 800px; }
.lede {
font-family: var(--display);
font-size: clamp(32px, 5.6vw, 54px);
line-height: 1.1;
letter-spacing: -0.02em;
font-weight: 500;
color: #ffffff;
text-wrap: balance;
}
.sublede {
margin-top: 24px;
font-size: clamp(16.5px, 2vw, 19px);
color: var(--slate-300);
max-width: 62ch;
}
.intro-note {
margin-top: 20px;
font-size: 16px;
color: var(--slate-400);
max-width: 62ch;
}
.spec-bar {
font-family: var(--mono);
font-size: 12.5px;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--slate-400);
display: flex;
flex-wrap: wrap;
gap: 10px 0;
}
.spec-bar span { padding: 0 18px; border-left: 1px solid var(--slate-700); }
.spec-bar span:first-child { padding-left: 0; border-left: none; }
.spec-bar .spec-accent { color: var(--volt-300); }
/* ---- Body ---- */
main {
max-width: 920px;
margin: 0 auto;
padding: 0 28px 72px;
}
section {
border-top: 1px solid var(--border);
padding: 52px 0 64px;
display: flex;
flex-wrap: wrap;
gap: 32px;
}
#services { border-top: none; padding-top: clamp(52px, 7vw, 76px); }
h2 {
margin: 0;
flex: 0 0 208px;
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-tertiary);
}
h2 .num { color: var(--accent-text); margin-right: 12px; }
.section-body { flex: 1 1 380px; max-width: 620px; }
/* Services */
.service-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 30px;
}
.service-list h3 {
margin: 0 0 6px;
font-family: var(--display);
font-size: 22px;
line-height: 1.3;
letter-spacing: -0.01em;
font-weight: 600;
}
.service-list p { color: var(--text-secondary); }
/* Engagement */
#engagement .section-body p + p { margin-top: 18px; }
.referral-note { color: var(--text-secondary); }
/* Track record */
.record-list { margin: 0; }
.record { padding: 24px 0 26px; }
.record:first-child { padding-top: 0; }
.record + .record { border-top: 1px solid var(--border-subtle); }
.record dt {
font-family: var(--mono);
font-size: 12.5px;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--text-tertiary);
margin-bottom: 6px;
}
.record .idx { color: var(--accent-text); margin-right: 6px; }
.record dd { margin: 0; font-size: 17.5px; }
.record strong { font-family: var(--display); font-weight: 600; }
/* Contact — ink console panel */
#contact {
display: block;
border-top: none;
background: var(--ink-2);
background-image:
linear-gradient(rgba(246,248,249,0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(246,248,249,0.05) 1px, transparent 1px);
background-size: 48px 48px;
border-left: 3px solid var(--volt-500);
border-radius: 16px;
padding: clamp(40px, 6vw, 72px);
color: #f6f8f9;
}
#contact h2 { color: var(--slate-400); margin-bottom: 20px; }
#contact h2 .num { color: var(--volt-400); }
.contact-address {
font-size: clamp(23px, 4vw, 38px);
line-height: 1.25;
}
.contact-address a {
font-family: var(--display);
font-weight: 600;
letter-spacing: -0.015em;
color: var(--volt-300);
text-decoration: none;
border-bottom: 2px solid var(--volt-500);
}
.contact-address a:hover { color: var(--volt-200); }
.contact-note {
margin-top: 20px;
color: var(--slate-300);
max-width: 56ch;
font-size: 16px;
}
/* Footer */
footer {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
flex-wrap: wrap;
padding-top: 28px;
font-family: var(--mono);
font-size: 12.5px;
color: var(--text-tertiary);
}
@media (max-width: 480px) {
main { padding: 0 20px 56px; }
.hero-inner { padding-left: 20px; padding-right: 20px; }
section { padding: 40px 0 48px; }
}
@@ -0,0 +1,27 @@
# Traefik file-provider router for phasefinal-web.
#
# DEPLOYS TO: ana-docker:/opt/docker/conf/traefik-ana/config/phasefinal-web.yml
# (NOT /opt/docker/conf/phasefinal-web/ — the file provider watches
# traefik-ana/config, so this file lives with Traefik, not the stack.)
#
# WHY A FILE ROUTER instead of container labels: as of 2026-08-29 the Traefik
# docker provider on ana-docker was not registering newly-created containers —
# phasefinal-web carried correct traefik.* labels, sat on traefik-net, and never
# appeared in /api/http/routers, with no provider activity in the logs. The file
# provider (watch: true) picks this up with no Traefik restart, so no other
# public service is interrupted. If the docker provider is fixed, this file can
# be deleted and the compose labels will take over.
http:
routers:
phasefinal-web:
rule: "Host(`www.phasefinal.com`)"
service: phasefinal-web
entryPoints:
- websecure
tls:
certResolver: anaprod
services:
phasefinal-web:
loadBalancer:
servers:
- url: "http://phasefinal-web:80"