diff --git a/.dockerignore b/.dockerignore
index 919fa26..0256fe1 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,3 +7,5 @@ content/*.mp4
content/*.pdf
*.md
!README.md
+docker/auth
+content/crowd
diff --git a/.gitignore b/.gitignore
index cc2d5e1..6f17b5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,6 @@ content/*.mov
# previz source renders (web versions in public/previz)
content/crowd/
+
+# server-side login files for /previz/ (see DEPLOY.md)
+docker/auth/
diff --git a/DEPLOY.md b/DEPLOY.md
index b21607b..7702fe0 100644
--- a/DEPLOY.md
+++ b/DEPLOY.md
@@ -112,6 +112,22 @@ The workflow is already in the repo (`.gitea/workflows/deploy.yml`). In Gitea, o
Test it: push any commit to `main` and watch **Actions** in the repo. The job should run
`git pull` and `docker compose up -d --build`, then the site updates.
+### 8. Password for /previz/
+
+Client previz pages under `/previz/` need a login (HTTP basic auth in `docker/nginx.conf`).
+The password file stays on the server and is never committed. Create it once:
+
+```bash
+cd /opt/ttvfx_web
+mkdir -p docker/auth
+printf "production:$(openssl passwd -apr1)\n" > docker/auth/previz.htpasswd # prompts for the password
+docker compose up -d --force-recreate
+```
+
+`production` is the username. To change the password, run the `printf` line again. nginx reads
+the file on every request, so no restart is needed. To add a second login, append another line
+with `>>`. If the file is missing, `/previz/` returns a 500 error, so it is never left open.
+
---
## Day to day
diff --git a/app/globals.css b/app/globals.css
index c43facc..05dbac1 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -89,6 +89,23 @@ body {
.reveal { opacity: 1; transform: none; transition: none; }
}
+/* Previz pages (/previz/*) print/PDF dark, edge to edge, like the screen version.
+ Margins come from padding cloned onto every page instead of white @page margins.
+ The page sets its own @page (landscape, no margin) so it doesn't get the report's. */
+@media print {
+ html:has(.previz), html:has(.previz) body { background: var(--ink) !important; color-scheme: dark; }
+ .previz {
+ padding-block: 12mm;
+ box-decoration-break: clone;
+ -webkit-box-decoration-break: clone;
+ print-color-adjust: exact;
+ -webkit-print-color-adjust: exact;
+ }
+ .previz.grain::after { display: none; }
+ .previz figure, .previz li, .previz img, .previz dl { break-inside: avoid; }
+ .previz h2 { break-after: avoid; }
+}
+
/* Printable VFX report (/set/report/view): A4 paper, keep the accent colours */
@media print {
@page { size: A4; margin: 14mm 12mm; background: white; }
diff --git a/app/previz/crowd/page.tsx b/app/previz/crowd/page.tsx
index 1d0d377..6bc1206 100644
--- a/app/previz/crowd/page.tsx
+++ b/app/previz/crowd/page.tsx
@@ -162,17 +162,25 @@ const setTips: Tip[] = [
function PreviewVideo({ src, poster, label }: { src?: string; poster?: string; label: string }) {
if (src) {
return (
-
+
+
+ {/* Paper/PDF: the poster frame and a pointer to the moving version */}
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+ {poster && }
+
The drone starts behind the audience and flies forward toward the truck stage. Height and camera tilt stay
@@ -297,7 +315,7 @@ export default function CrowdPrevizPage() {
-
+
After the hero shot, the drone flies to a single marked position and holds still. No camera movement. The
@@ -326,7 +344,7 @@ export default function CrowdPrevizPage() {
-
+
We track the hero drone move in 3D, then place each plate as a flat layer on the ground behind the real
@@ -340,7 +358,7 @@ export default function CrowdPrevizPage() {
-
+ Shot list
Six shots, two setups
@@ -351,7 +369,7 @@ export default function CrowdPrevizPage() {
-
+ Crowd direction
Briefing the extras
@@ -385,16 +403,16 @@ export default function CrowdPrevizPage() {
the comp, so that's where arms need to stay down.
-
+