Compare commits
14 Commits
archive/se
...
918c000817
| Author | SHA1 | Date | |
|---|---|---|---|
| 918c000817 | |||
| 1ddde1ffd5 | |||
| a795fb345f | |||
| 094fdbea51 | |||
| 1c297b336f | |||
| a5c15b4c7a | |||
| b530b7a76f | |||
| 7134b70db9 | |||
| c4718658d4 | |||
| 38c0c5031e | |||
| d2badf49d4 | |||
| af0ed66122 | |||
| 7765d0fab2 | |||
| fba223004f |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,9 @@ ehthumbs.db
|
|||||||
|
|
||||||
# --- Claude Code local config (per-developer) ---
|
# --- Claude Code local config (per-developer) ---
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
# .mcp.json holds the WP Application Password for the Bricks MCP server,
|
||||||
|
# which is per-developer and must never reach the remote.
|
||||||
|
.mcp.json
|
||||||
|
|
||||||
# --- Environment / secrets ---
|
# --- Environment / secrets ---
|
||||||
.env
|
.env
|
||||||
|
|||||||
81
Bricks Config/bricks-styles-color-palette-asterion.json
Normal file
81
Bricks Config/bricks-styles-color-palette-asterion.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"id": "a5701b",
|
||||||
|
"name": "Asterion",
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-brand-navy)",
|
||||||
|
"id": "9c1d2a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#081427",
|
||||||
|
"raw": "var(--color-brand-navy-deep)",
|
||||||
|
"id": "1f4b88"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#C9A45A",
|
||||||
|
"raw": "var(--color-brand-gold)",
|
||||||
|
"id": "e74c3b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#E0C892",
|
||||||
|
"raw": "var(--color-brand-gold-soft)",
|
||||||
|
"id": "62a190"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#C8102E",
|
||||||
|
"raw": "var(--color-alert-red)",
|
||||||
|
"id": "84d709"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-text-primary)",
|
||||||
|
"id": "3fb52c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#3E4C5E",
|
||||||
|
"raw": "var(--color-text-secondary)",
|
||||||
|
"id": "57e441"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#6B7785",
|
||||||
|
"raw": "var(--color-text-muted)",
|
||||||
|
"id": "ad8b16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#FFFFFF",
|
||||||
|
"raw": "var(--color-text-on-dark)",
|
||||||
|
"id": "c9027d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#FFFFFF",
|
||||||
|
"raw": "var(--color-bg-default)",
|
||||||
|
"id": "20fa53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#F5F7FA",
|
||||||
|
"raw": "var(--color-bg-subtle)",
|
||||||
|
"id": "8e6c0f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-bg-dark)",
|
||||||
|
"id": "4d318a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#13294B",
|
||||||
|
"raw": "var(--color-bg-dark-elevated)",
|
||||||
|
"id": "f1957e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#CCD3DC",
|
||||||
|
"raw": "var(--color-border-default)",
|
||||||
|
"id": "7b46c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#1E5BA8",
|
||||||
|
"raw": "var(--color-info)",
|
||||||
|
"id": "06bdfa"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
96
Bricks Config/bricks-styles-color-palette-default.json
Normal file
96
Bricks Config/bricks-styles-color-palette-default.json
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"id": "44a09f",
|
||||||
|
"name": "Default",
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"light": "#f5f5f5",
|
||||||
|
"raw": "var(--bricks-color-grey-100)",
|
||||||
|
"id": "ab2156"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#e0e0e0",
|
||||||
|
"raw": "var(--bricks-color-grey-300)",
|
||||||
|
"id": "664644"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#9e9e9e",
|
||||||
|
"raw": "var(--bricks-color-grey-500)",
|
||||||
|
"id": "eec8d2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#616161",
|
||||||
|
"raw": "var(--bricks-color-grey-700)",
|
||||||
|
"id": "68168c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#424242",
|
||||||
|
"raw": "var(--bricks-color-grey-800)",
|
||||||
|
"id": "5ce47e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#212121",
|
||||||
|
"raw": "var(--bricks-color-grey-900)",
|
||||||
|
"id": "f5049f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ffeb3b",
|
||||||
|
"raw": "var(--bricks-color-yellow)",
|
||||||
|
"id": "9d011c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ffc107",
|
||||||
|
"raw": "var(--bricks-color-amber)",
|
||||||
|
"id": "dacd30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ff9800",
|
||||||
|
"raw": "var(--bricks-color-orange)",
|
||||||
|
"id": "3bcffd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ff5722",
|
||||||
|
"raw": "var(--bricks-color-deep-orange)",
|
||||||
|
"id": "2ce1e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#f44336",
|
||||||
|
"raw": "var(--bricks-color-red)",
|
||||||
|
"id": "57b992"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#9c27b0",
|
||||||
|
"raw": "var(--bricks-color-purple)",
|
||||||
|
"id": "5825c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#2196f3",
|
||||||
|
"raw": "var(--bricks-color-blue)",
|
||||||
|
"id": "0e5dd9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#03a9f4",
|
||||||
|
"raw": "var(--bricks-color-light-blue)",
|
||||||
|
"id": "f5d57b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#81D4FA",
|
||||||
|
"raw": "var(--bricks-color-sky-blue)",
|
||||||
|
"id": "7dd108"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#4caf50",
|
||||||
|
"raw": "var(--bricks-color-green)",
|
||||||
|
"id": "5c8f35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#8bc34a",
|
||||||
|
"raw": "var(--bricks-color-light-green)",
|
||||||
|
"id": "432723"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#cddc39",
|
||||||
|
"raw": "var(--bricks-color-lime)",
|
||||||
|
"id": "25a3a8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
179
Bricks Config/color-palette-current.json
Normal file
179
Bricks Config/color-palette-current.json
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "44a09f",
|
||||||
|
"name": "Default",
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"light": "#f5f5f5",
|
||||||
|
"raw": "var(--bricks-color-grey-100)",
|
||||||
|
"id": "ab2156"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#e0e0e0",
|
||||||
|
"raw": "var(--bricks-color-grey-300)",
|
||||||
|
"id": "664644"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#9e9e9e",
|
||||||
|
"raw": "var(--bricks-color-grey-500)",
|
||||||
|
"id": "eec8d2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#616161",
|
||||||
|
"raw": "var(--bricks-color-grey-700)",
|
||||||
|
"id": "68168c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#424242",
|
||||||
|
"raw": "var(--bricks-color-grey-800)",
|
||||||
|
"id": "5ce47e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#212121",
|
||||||
|
"raw": "var(--bricks-color-grey-900)",
|
||||||
|
"id": "f5049f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ffeb3b",
|
||||||
|
"raw": "var(--bricks-color-yellow)",
|
||||||
|
"id": "9d011c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ffc107",
|
||||||
|
"raw": "var(--bricks-color-amber)",
|
||||||
|
"id": "dacd30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ff9800",
|
||||||
|
"raw": "var(--bricks-color-orange)",
|
||||||
|
"id": "3bcffd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ff5722",
|
||||||
|
"raw": "var(--bricks-color-deep-orange)",
|
||||||
|
"id": "2ce1e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#f44336",
|
||||||
|
"raw": "var(--bricks-color-red)",
|
||||||
|
"id": "57b992"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#9c27b0",
|
||||||
|
"raw": "var(--bricks-color-purple)",
|
||||||
|
"id": "5825c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#2196f3",
|
||||||
|
"raw": "var(--bricks-color-blue)",
|
||||||
|
"id": "0e5dd9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#03a9f4",
|
||||||
|
"raw": "var(--bricks-color-light-blue)",
|
||||||
|
"id": "f5d57b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#81D4FA",
|
||||||
|
"raw": "var(--bricks-color-sky-blue)",
|
||||||
|
"id": "7dd108"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#4caf50",
|
||||||
|
"raw": "var(--bricks-color-green)",
|
||||||
|
"id": "5c8f35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#8bc34a",
|
||||||
|
"raw": "var(--bricks-color-light-green)",
|
||||||
|
"id": "432723"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#cddc39",
|
||||||
|
"raw": "var(--bricks-color-lime)",
|
||||||
|
"id": "25a3a8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rudrjy",
|
||||||
|
"name": "Asterion",
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-brand-navy)",
|
||||||
|
"id": "9c1d2a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#081427",
|
||||||
|
"raw": "var(--color-brand-navy-deep)",
|
||||||
|
"id": "1f4b88"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#C9A45A",
|
||||||
|
"raw": "var(--color-brand-gold)",
|
||||||
|
"id": "e74c3b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#E0C892",
|
||||||
|
"raw": "var(--color-brand-gold-soft)",
|
||||||
|
"id": "62a190"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#C8102E",
|
||||||
|
"raw": "var(--color-alert-red)",
|
||||||
|
"id": "84d709"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-text-primary)",
|
||||||
|
"id": "3fb52c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#3E4C5E",
|
||||||
|
"raw": "var(--color-text-secondary)",
|
||||||
|
"id": "57e441"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#6B7785",
|
||||||
|
"raw": "var(--color-text-muted)",
|
||||||
|
"id": "ad8b16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#FFFFFF",
|
||||||
|
"raw": "var(--color-text-on-dark)",
|
||||||
|
"id": "c9027d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#FFFFFF",
|
||||||
|
"raw": "var(--color-bg-default)",
|
||||||
|
"id": "20fa53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#F5F7FA",
|
||||||
|
"raw": "var(--color-bg-subtle)",
|
||||||
|
"id": "8e6c0f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-bg-dark)",
|
||||||
|
"id": "4d318a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#13294B",
|
||||||
|
"raw": "var(--color-bg-dark-elevated)",
|
||||||
|
"id": "f1957e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#CCD3DC",
|
||||||
|
"raw": "var(--color-border-default)",
|
||||||
|
"id": "7b46c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#1E5BA8",
|
||||||
|
"raw": "var(--color-info)",
|
||||||
|
"id": "06bdfa"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
7
Bricks Config/global-settings-current.json
Normal file
7
Bricks Config/global-settings-current.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"postTypes": [
|
||||||
|
"page"
|
||||||
|
],
|
||||||
|
"builderMode": "dark",
|
||||||
|
"builderToolbarLogoLink": "current"
|
||||||
|
}
|
||||||
36
Bricks Config/theme-styles-current.json
Normal file
36
Bricks Config/theme-styles-current.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"asterion": {
|
||||||
|
"label": "Asterion",
|
||||||
|
"settings": {
|
||||||
|
"_custom": true,
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "skuksb",
|
||||||
|
"main": "any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"colorPrimary": {
|
||||||
|
"raw": "var(--color-brand-navy)"
|
||||||
|
},
|
||||||
|
"colorSecondary": {
|
||||||
|
"raw": "var(--color-brand-gold)"
|
||||||
|
},
|
||||||
|
"colorLight": {
|
||||||
|
"raw": "var(--color-bg-default)"
|
||||||
|
},
|
||||||
|
"colorDark": {
|
||||||
|
"raw": "var(--color-bg-dark)"
|
||||||
|
},
|
||||||
|
"colorMuted": {
|
||||||
|
"raw": "var(--color-text-muted)"
|
||||||
|
},
|
||||||
|
"colorBorder": {
|
||||||
|
"raw": "var(--color-border-default)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
README.md
41
README.md
@@ -62,31 +62,46 @@ Puis dans WP Admin → Apparence → Thèmes, activer **Asterion Bricks** (le ch
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Approche
|
||||||
|
|
||||||
|
**Bricks-first, child theme minimaliste.** Le rendu de chaque page est piloté par Bricks Builder (Templates pour header/footer/archives + pages éditées dans le builder). Le child theme se contente d'apporter le design system (tokens CSS), Inter self-hosté (RGPD) et les Custom Post Types. Voir `wp-content/themes/asterion-bricks/README.md` pour le détail.
|
||||||
|
|
||||||
|
Une session de PHP-rendering plus traditionnelle a été archivée dans la branche Git `archive/session-2-php-templates` ainsi que dans `RESTART-PLAN.md` à la racine. Ne pas réintroduire ce pattern sans relire ces deux sources.
|
||||||
|
|
||||||
## Structure du repo
|
## Structure du repo
|
||||||
|
|
||||||
```
|
```
|
||||||
WP2026/
|
WP2026/
|
||||||
├── BRIEF-Claude-Code.md # brief projet (lecture obligatoire)
|
├── BRIEF-Claude-Code.md # brief projet (lecture obligatoire)
|
||||||
├── README.md # ce fichier
|
├── README.md # ce fichier
|
||||||
|
├── RESTART-PLAN.md # historique de la décision Bricks-first
|
||||||
├── MISSING-ASSETS.md # tracking des visuels/medias manquants
|
├── MISSING-ASSETS.md # tracking des visuels/medias manquants
|
||||||
├── *.pdf # 2 livrables stratégie + design (sources de vérité)
|
├── *.pdf # 2 livrables stratégie + design (sources de vérité)
|
||||||
├── ThirdParty/ # gitignored — ZIP plugins commerciaux à déposer manuellement
|
├── ThirdParty/ # gitignored — ZIP plugins commerciaux
|
||||||
│ ├── bricks.*.zip
|
│ ├── bricks.*.zip
|
||||||
│ └── sitepress-multilingual-cms.*.zip
|
│ └── sitepress-multilingual-cms.*.zip
|
||||||
|
├── content-source/ # copy EN extrait du PDF (référence + génération)
|
||||||
|
│ ├── solutions-data.php
|
||||||
|
│ ├── industries-data.php
|
||||||
|
│ ├── technology-data.php
|
||||||
|
│ ├── editorial-data.php
|
||||||
|
│ ├── forms-data.php
|
||||||
|
│ └── README.md
|
||||||
└── wp-content/
|
└── wp-content/
|
||||||
└── themes/
|
└── themes/
|
||||||
└── asterion-bricks/ # le child theme — coeur du travail
|
└── asterion-bricks/ # child theme minimaliste, voir son README
|
||||||
├── style.css # en-tête WP + import tokens
|
├── style.css
|
||||||
├── functions.php # hooks, registers, queries
|
├── functions.php
|
||||||
├── theme.json # tokens globaux (Gutenberg + Bricks)
|
├── theme.json
|
||||||
├── assets/
|
├── README.md
|
||||||
│ ├── css/ # tokens.css, components.css, utilities.css
|
├── inc/
|
||||||
│ ├── js/ # vanilla JS, defer
|
│ └── cpt.php
|
||||||
│ ├── fonts/ # Inter + Inter Display self-hosted (RGPD)
|
└── assets/
|
||||||
│ └── img/
|
├── css/
|
||||||
├── templates/ # exports Bricks JSON (versionnés)
|
│ ├── tokens.css # design system, source de vérité
|
||||||
├── inc/ # CPT, ACF field groups, SEO schema
|
│ └── utilities.css # skip-link, sr-only, focus-visible
|
||||||
└── README.md
|
├── fonts/ # Inter Variable self-hosté
|
||||||
|
└── img/
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
52
content-source/README.md
Normal file
52
content-source/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Content source — copy reference
|
||||||
|
|
||||||
|
PHP arrays of all the textual content authored in the Strategy & Content PDF (`Asterion-VR_Refonte-2026_Strategie-et-Contenu.pdf`, sections D.1 to D.29). Each file holds the EN copy for one site section, keyed by page slug.
|
||||||
|
|
||||||
|
These files are **not** loaded at runtime. They serve two purposes:
|
||||||
|
|
||||||
|
1. **Reference while building Bricks pages** — open the file matching the page you're editing and copy/paste the EN copy into the Bricks elements (Heading, Basic Text, Button…).
|
||||||
|
2. **Source for programmatic generation** — once a Bricks archetype template is built and validated visually, a one-shot script can iterate over the data here, clone the template, swap in each variant's strings, and create the WP page programmatically. This avoids rebuilding the same layout 4–6 times for similar pages (e.g. the 4 industry detail pages).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Pages covered |
|
||||||
|
|---|---|
|
||||||
|
| `solutions-data.php` | `/solutions/my-proserve/`, `/solutions/proserve-flex/`, `/solutions/proserve-academy/`, `/solutions/customization/` |
|
||||||
|
| `industries-data.php` | `/industries/police/`, `/industries/special-forces/`, `/industries/military/`, `/industries/firefighters/` |
|
||||||
|
| `technology-data.php` | 6 technology pillars under `/technology/` |
|
||||||
|
| `editorial-data.php` | Why Asterion, About, Customers hub, Trust, Partners |
|
||||||
|
| `forms-data.php` | Conversion forms (Demo, T&E Kit, Quote, Contact) |
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
Every entry follows roughly the same shape :
|
||||||
|
|
||||||
|
```php
|
||||||
|
'slug-of-page' => [
|
||||||
|
'eyebrow' => 'Small overline above the H1',
|
||||||
|
'title' => 'Short page title',
|
||||||
|
'tagline' => 'Hero H1',
|
||||||
|
'meta_title' => 'For <head><title> + OpenGraph',
|
||||||
|
'meta_description' => 'For <head> meta description',
|
||||||
|
'hero_lead' => 'Long sub-headline under the H1',
|
||||||
|
'ctas' => [['label' => '...', 'href' => '/...', 'variant' => 'primary'], …],
|
||||||
|
'sections' => [
|
||||||
|
['type' => 'prose' , 'eyebrow' => '…', 'title' => '…', 'body' => '…'],
|
||||||
|
['type' => 'cards' , 'eyebrow' => '…', 'title' => '…', 'items' => [['title' => '…', 'body' => '…'], …]],
|
||||||
|
['type' => 'list' , 'eyebrow' => '…', 'title' => '…', 'items' => ['line 1', 'line 2', …]],
|
||||||
|
['type' => 'cta' , 'eyebrow' => '…', 'title' => '…', 'body' => '…', 'ctas' => […]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
`forms-data.php` deviates slightly — see its inline header comment.
|
||||||
|
|
||||||
|
## Why we keep these in PHP arrays (vs JSON / YAML / Markdown)
|
||||||
|
|
||||||
|
- They were extracted programmatically from the PDF and were already used by the previous PHP-rendering approach. Keeping the format means zero re-extraction work.
|
||||||
|
- A future PHP migration script will `include $file_path` and use the returned array directly — no parsing layer required.
|
||||||
|
- For the human reading them while building in Bricks, plain PHP arrays are perfectly readable.
|
||||||
|
|
||||||
|
## French translations
|
||||||
|
|
||||||
|
These files are **EN only**. The FR translations will live inside WordPress, written by the linguist via WPML's Translation Editor against each Bricks-edited page.
|
||||||
69
tools/README.md
Normal file
69
tools/README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Tools
|
||||||
|
|
||||||
|
One-shot scripts and helpers used during project setup or migrations. They are NOT part of the published site.
|
||||||
|
|
||||||
|
## fresh-setup-mu-plugin.php
|
||||||
|
|
||||||
|
Drop into `wp-content/mu-plugins/` of a freshly created LocalWP site to auto-configure WordPress for the Asterion VR project. Read its inline header for the full procedure.
|
||||||
|
|
||||||
|
Quick recap :
|
||||||
|
|
||||||
|
1. Delete the old LocalWP site (right-click → Delete site, optionally move folder to trash).
|
||||||
|
2. Create a new LocalWP site named "Asterion 2026", mode `localhost`, default credentials.
|
||||||
|
3. Note the allocated port — likely `10004` again. **If different, edit `AV_TARGET_HOME` in `fresh-setup-mu-plugin.php` first.**
|
||||||
|
4. Junction the child theme into the new site:
|
||||||
|
```powershell
|
||||||
|
New-Item -ItemType Junction `
|
||||||
|
-Path "C:\Users\j_foucher\Local Sites\asterion-2026\app\public\wp-content\themes\asterion-bricks" `
|
||||||
|
-Target "C:\ASTERION\GIT\WP2026\wp-content\themes\asterion-bricks"
|
||||||
|
```
|
||||||
|
5. Extract the Bricks parent **theme** into `wp-content/themes/`. Bricks is a theme (not a plugin) — installing the ZIP via WP admin → Plugins → Add New will fail with "No valid plugins were found".
|
||||||
|
```powershell
|
||||||
|
$site = "C:\Users\j_foucher\Local Sites\asterion-2026\app\public\wp-content"
|
||||||
|
Expand-Archive "C:\ASTERION\GIT\WP2026\ThirdParty\bricks.2.3.4.zip" "$site\themes\" -Force
|
||||||
|
```
|
||||||
|
(Or via WP admin → Appearance → Themes → Add New Theme → Upload Theme.)
|
||||||
|
6. Extract WPML **plugin** into `wp-content/plugins/`:
|
||||||
|
```powershell
|
||||||
|
Expand-Archive "C:\ASTERION\GIT\WP2026\ThirdParty\sitepress-multilingual-cms.4.9.3.zip" "$site\plugins\" -Force
|
||||||
|
```
|
||||||
|
(Or via WP admin → Plugins → Add New → Upload Plugin.)
|
||||||
|
7. Copy `tools/fresh-setup-mu-plugin.php` → `<NEW_SITE>\app\public\wp-content\mu-plugins\fresh-setup-mu-plugin.php`
|
||||||
|
8. Visit `http://localhost:10004/` once. The plugin runs at `init` priority 50, configures everything, and self-deletes after success.
|
||||||
|
9. Verify by hitting `http://localhost:10004/wp-admin` — login, then check that:
|
||||||
|
- Theme : Asterion Bricks active (parent: Bricks)
|
||||||
|
- Plugins : WPML active
|
||||||
|
- Settings → Permalinks : Post name (`/%postname%/`)
|
||||||
|
- Bricks → Settings → Builder access : Page + Post + case_study + scenario checked
|
||||||
|
|
||||||
|
Then activate the Bricks license (Bricks → License) and run the WPML setup wizard manually — those need your license keys and an interactive choice flow that's safer to do in admin.
|
||||||
|
|
||||||
|
## av-apply-bricks-styles.php
|
||||||
|
|
||||||
|
One-shot mu-plugin that writes the full Asterion Theme Style to `wp_options` (`bricks_theme_styles`) AND fixes `bricks_global_settings.postTypes` to include `page, post, case_study, scenario`.
|
||||||
|
|
||||||
|
Why a mu-plugin : Bricks stores Theme Styles as serialized PHP in the DB (not in a file), so we need code that runs in the WP context. Mu-plugin URL trigger is the simplest path; LocalWP CLI PHP can't connect to the per-site MySQL socket without extra config.
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
1. Copy this file → `<LocalWP site>\app\public\wp-content\mu-plugins\av-apply-bricks-styles.php`
|
||||||
|
2. Visit `http://localhost:10004/?av_apply_styles=1` (logged in as admin)
|
||||||
|
3. A dark-themed report shows what was written.
|
||||||
|
4. Reload the Bricks builder (Ctrl+R) → Theme Style "Asterion" applied to entire website.
|
||||||
|
|
||||||
|
Idempotent — re-running overwrites with the same payload. Edit the file then re-trigger to iterate.
|
||||||
|
|
||||||
|
Sets : General (site bg), Colors (11 semantic roles), Links (gold + underline), Typography (Inter, body 16px, h1-h6 sizes from `tokens.css`), Button (primary navy, secondary gold, padding scale), Section (5rem padding), Container (max 1280px), Focus outline.
|
||||||
|
|
||||||
|
## av-dump-bricks.php
|
||||||
|
|
||||||
|
One-shot mu-plugin that exports the live `bricks_theme_styles`, `bricks_color_palette`, `bricks_global_settings` options to JSON files in `Bricks Config/` for inspection or git-versioned snapshots.
|
||||||
|
|
||||||
|
Usage : drop into mu-plugins, visit `?av_dump_bricks=1` (admin), three JSONs appear in `Bricks Config/`.
|
||||||
|
|
||||||
|
## What's missing — manual follow-up after fresh setup
|
||||||
|
|
||||||
|
- Bricks license key activation (Bricks → License)
|
||||||
|
- WPML setup wizard (WPML → Setup) : EN default + FR secondary, URL strategy = "Different languages in directories"
|
||||||
|
- Run `?av_apply_styles=1` (see above) to push the Theme Style — replaces clicking through Bricks → Theme Styles.
|
||||||
|
- Build the Header and Footer Bricks Templates (per RESTART-PLAN.md Phase 4)
|
||||||
1573
tools/_av-bricks-shared.php
Normal file
1573
tools/_av-bricks-shared.php
Normal file
File diff suppressed because it is too large
Load Diff
151
tools/av-add-submenus.php
Normal file
151
tools/av-add-submenus.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Add submenus to Header — Main + style nav-menu dropdown
|
||||||
|
* /?av_add_submenus=1
|
||||||
|
* 1. For each of Solutions, Industries, Technology (top-level items in
|
||||||
|
* Header — Main, term_id=9), adds child menu items pointing to the
|
||||||
|
* respective detail pages.
|
||||||
|
* 2. Updates the Bricks nav-menu element settings (hdrnav in template 25)
|
||||||
|
* with dropdown styling : navy background, gold hover, padding.
|
||||||
|
*
|
||||||
|
* Idempotent : skips children that already exist (matched by URL).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (empty($_GET['av_add_submenus'])) return;
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
1. Map slug → list of children for each top-level URL
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
$sub_menus = [
|
||||||
|
'/solutions/' => [
|
||||||
|
['title' => 'MY PROSERVE', 'url' => '/solutions/my-proserve/'],
|
||||||
|
['title' => 'PROSERVE FLEX', 'url' => '/solutions/proserve-flex/'],
|
||||||
|
['title' => 'PROSERVE ACADEMY', 'url' => '/solutions/proserve-academy/'],
|
||||||
|
['title' => 'PROSERVE+', 'url' => '/solutions/customization/'],
|
||||||
|
],
|
||||||
|
'/industries/' => [
|
||||||
|
['title' => 'Police', 'url' => '/industries/police/'],
|
||||||
|
['title' => 'Special Forces', 'url' => '/industries/special-forces/'],
|
||||||
|
['title' => 'Military', 'url' => '/industries/military/'],
|
||||||
|
['title' => 'Firefighters', 'url' => '/industries/firefighters/'],
|
||||||
|
],
|
||||||
|
'/technology/' => [
|
||||||
|
['title' => 'VR Hardware', 'url' => '/technology/vr-hardware/'],
|
||||||
|
['title' => 'Software & AI', 'url' => '/technology/software-ai/'],
|
||||||
|
['title' => 'Weapons & Tracking', 'url' => '/technology/weapons-tracking/'],
|
||||||
|
['title' => 'Scenarios', 'url' => '/technology/scenarios/'],
|
||||||
|
['title' => 'Instructor Cockpit', 'url' => '/technology/instructor-cockpit/'],
|
||||||
|
['title' => 'After-Action Review', 'url' => '/technology/after-action-review/'],
|
||||||
|
],
|
||||||
|
'/why-asterion/' => [
|
||||||
|
['title' => 'About', 'url' => '/about/'],
|
||||||
|
['title' => 'Customers', 'url' => '/customers/'],
|
||||||
|
['title' => 'Partners', 'url' => '/partners/'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$term_id = 9; // Header — Main
|
||||||
|
$existing = wp_get_nav_menu_items($term_id) ?: [];
|
||||||
|
$by_url = [];
|
||||||
|
foreach ($existing as $item) {
|
||||||
|
$by_url[$item->url] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_added = 0;
|
||||||
|
foreach ($sub_menus as $parent_url => $children) {
|
||||||
|
if (! isset($by_url[$parent_url])) {
|
||||||
|
echo " Parent $parent_url not found in menu — skipping\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parent_id = $by_url[$parent_url]->ID;
|
||||||
|
echo "── Parent '{$by_url[$parent_url]->title}' (#$parent_id, url=$parent_url) ──\n";
|
||||||
|
|
||||||
|
foreach ($children as $child) {
|
||||||
|
if (isset($by_url[$child['url']])) {
|
||||||
|
echo " '{$child['title']}' ({$child['url']}) already exists (#{$by_url[$child['url']]->ID}) — skip\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$new_id = wp_update_nav_menu_item($term_id, 0, [
|
||||||
|
'menu-item-title' => $child['title'],
|
||||||
|
'menu-item-url' => $child['url'],
|
||||||
|
'menu-item-type' => 'custom',
|
||||||
|
'menu-item-status' => 'publish',
|
||||||
|
'menu-item-parent-id' => $parent_id,
|
||||||
|
]);
|
||||||
|
if (is_wp_error($new_id) || ! $new_id) {
|
||||||
|
echo " '{$child['title']}' : FAILED\n";
|
||||||
|
} else {
|
||||||
|
echo " '{$child['title']}' → #$new_id (parent=$parent_id)\n";
|
||||||
|
$total_added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Added $total_added new menu item(s).\n\n";
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
2. Patch nav-menu element 'hdrnav' settings : dropdown styling
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
global $wpdb;
|
||||||
|
foreach (['_bricks_page_content_2', '_bricks_page_header_2'] as $key) {
|
||||||
|
$val = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
||||||
|
25, $key
|
||||||
|
));
|
||||||
|
if (! is_string($val) || $val === '') continue;
|
||||||
|
$is_json = ($val[0] === '[');
|
||||||
|
$elements = $is_json ? json_decode($val, true) : @unserialize($val);
|
||||||
|
if (! is_array($elements)) continue;
|
||||||
|
|
||||||
|
$modified = false;
|
||||||
|
foreach ($elements as $i => $el) {
|
||||||
|
if (($el['id'] ?? '') !== 'hdrnav') continue;
|
||||||
|
|
||||||
|
// Dropdown background + padding + typography
|
||||||
|
$elements[$i]['settings']['subMenuBackgroundColor'] = ['hex' => '#0B1F3A', 'raw' => 'var(--color-brand-navy)'];
|
||||||
|
$elements[$i]['settings']['subMenuBorder'] = [
|
||||||
|
'width' => ['top' => '1px', 'right' => '1px', 'bottom' => '1px', 'left' => '1px'],
|
||||||
|
'color' => ['raw' => 'rgba(201, 164, 90, 0.2)'],
|
||||||
|
'style' => 'solid',
|
||||||
|
];
|
||||||
|
$elements[$i]['settings']['subMenuPadding'] = [
|
||||||
|
'top' => '0.5rem', 'right' => '0', 'bottom' => '0.5rem', 'left' => '0',
|
||||||
|
];
|
||||||
|
$elements[$i]['settings']['subMenuItemPadding'] = [
|
||||||
|
'top' => '0.6rem', 'right' => '1.25rem', 'bottom' => '0.6rem', 'left' => '1.25rem',
|
||||||
|
];
|
||||||
|
$elements[$i]['settings']['subMenuTypography'] = [
|
||||||
|
'color' => ['raw' => 'var(--color-text-on-dark)'],
|
||||||
|
'font-size' => '0.875rem',
|
||||||
|
'font-weight' => '500',
|
||||||
|
'text-decoration' => 'none',
|
||||||
|
'white-space' => 'nowrap',
|
||||||
|
];
|
||||||
|
$elements[$i]['settings']['subMenuItemHoverTypography'] = [
|
||||||
|
'color' => ['raw' => 'var(--color-brand-gold)'],
|
||||||
|
];
|
||||||
|
$elements[$i]['settings']['subMenuMinWidth'] = '14rem';
|
||||||
|
// Show arrow indicator next to parents with submenu
|
||||||
|
$elements[$i]['settings']['subMenuIndicator'] = true;
|
||||||
|
|
||||||
|
$modified = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($modified) {
|
||||||
|
$new = $is_json ? wp_json_encode($elements) : maybe_serialize($elements);
|
||||||
|
$wpdb->update($wpdb->postmeta, ['meta_value' => $new],
|
||||||
|
['post_id' => 25, 'meta_key' => $key], ['%s'], ['%d', '%s']);
|
||||||
|
wp_cache_delete(25, 'post_meta');
|
||||||
|
echo " $key : hdrnav settings updated (dropdown styling)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Reload the site → hover Solutions/Industries/Technology in header → submenu drops down.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
200
tools/av-apply-bricks-styles.php
Normal file
200
tools/av-apply-bricks-styles.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Apply Bricks Theme Style (one-shot)
|
||||||
|
* Description: Visit /?av_apply_styles=1 (admin only) → writes the full Asterion Theme Style + fixes Bricks postTypes. Idempotent.
|
||||||
|
*
|
||||||
|
* Sources of truth :
|
||||||
|
* - tokens.css (wp-content/themes/asterion-bricks/assets/css/tokens.css)
|
||||||
|
* - Design Handoff PDF section 2 (typography scale, line heights)
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_apply_styles'])) return;
|
||||||
|
if (! current_user_can('manage_options')) wp_die('admin only');
|
||||||
|
|
||||||
|
$report = [];
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------
|
||||||
|
1. Bricks Global Settings : add post + CPT to builder access
|
||||||
|
--------------------------------------------------------------- */
|
||||||
|
$global = get_option('bricks_global_settings', []);
|
||||||
|
if (! is_array($global)) $global = [];
|
||||||
|
$global['postTypes'] = ['page', 'post', 'case_study', 'scenario'];
|
||||||
|
update_option('bricks_global_settings', $global);
|
||||||
|
$report[] = "bricks_global_settings.postTypes = " . implode(',', $global['postTypes']);
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------
|
||||||
|
2. Bricks Theme Styles : full Asterion config
|
||||||
|
--------------------------------------------------------------- */
|
||||||
|
$existing = get_option('bricks_theme_styles', []);
|
||||||
|
$cond_id = $existing['asterion']['settings']['conditions']['conditions'][0]['id'] ?? 'skuksb';
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
$color = fn($var) => ['raw' => $var];
|
||||||
|
$sp = fn($t,$r,$b,$l) => ['top'=>$t,'right'=>$r,'bottom'=>$b,'left'=>$l];
|
||||||
|
|
||||||
|
$styles = [
|
||||||
|
'asterion' => [
|
||||||
|
'label' => 'Asterion',
|
||||||
|
'settings' => [
|
||||||
|
'_custom' => true,
|
||||||
|
|
||||||
|
'conditions' => [
|
||||||
|
'conditions' => [
|
||||||
|
['id' => $cond_id, 'main' => 'any'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- GENERAL : site background ---- */
|
||||||
|
'general' => [
|
||||||
|
'siteBackground' => [
|
||||||
|
'color' => $color('var(--color-bg-default)'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- COLORS : 11 semantic roles ---- */
|
||||||
|
'colors' => [
|
||||||
|
'colorPrimary' => $color('var(--color-brand-navy)'),
|
||||||
|
'colorSecondary' => $color('var(--color-brand-gold)'),
|
||||||
|
'colorLight' => $color('var(--color-bg-default)'),
|
||||||
|
'colorDark' => $color('var(--color-bg-dark)'),
|
||||||
|
'colorMuted' => $color('var(--color-text-muted)'),
|
||||||
|
'colorBorder' => $color('var(--color-border-default)'),
|
||||||
|
'colorInfo' => $color('var(--color-info)'),
|
||||||
|
'colorSuccess' => $color('var(--color-success)'),
|
||||||
|
'colorWarning' => $color('var(--color-warning)'),
|
||||||
|
'colorDanger' => $color('var(--color-alert-red)'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- LINKS : gold + underline ---- */
|
||||||
|
'links' => [
|
||||||
|
'typography' => [
|
||||||
|
'color' => $color('var(--color-brand-gold)'),
|
||||||
|
'text-decoration' => 'underline',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- TYPOGRAPHY ---- */
|
||||||
|
'typography' => [
|
||||||
|
// HTML font-size: 100% so 1rem = 16px
|
||||||
|
'typographyHtml' => '100%',
|
||||||
|
|
||||||
|
'typographyBody' => [
|
||||||
|
'font-family' => 'Inter',
|
||||||
|
'font-weight' => '400',
|
||||||
|
'font-size' => '1rem',
|
||||||
|
'line-height' => '1.6',
|
||||||
|
'letter-spacing' => '0',
|
||||||
|
'color' => $color('var(--color-text-primary)'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'typographyHeadings' => [
|
||||||
|
'font-family' => 'Inter',
|
||||||
|
'font-weight' => '700',
|
||||||
|
'letter-spacing' => '-0.01em',
|
||||||
|
'color' => $color('var(--color-text-primary)'),
|
||||||
|
],
|
||||||
|
|
||||||
|
// H1 : 36px / line 1.20
|
||||||
|
'typographyHeadingH1' => [
|
||||||
|
'font-size' => '2.25rem',
|
||||||
|
'line-height' => '1.20',
|
||||||
|
],
|
||||||
|
'h1Margin' => $sp('0', '0', '1.5rem', '0'),
|
||||||
|
|
||||||
|
// H2 : 28px / line 1.25
|
||||||
|
'typographyHeadingH2' => [
|
||||||
|
'font-size' => '1.75rem',
|
||||||
|
'line-height' => '1.25',
|
||||||
|
],
|
||||||
|
'h2Margin' => $sp('0', '0', '1.25rem', '0'),
|
||||||
|
|
||||||
|
// H3 : 22px / line 1.30
|
||||||
|
'typographyHeadingH3' => [
|
||||||
|
'font-size' => '1.375rem',
|
||||||
|
'line-height' => '1.30',
|
||||||
|
],
|
||||||
|
'h3Margin' => $sp('0', '0', '1rem', '0'),
|
||||||
|
|
||||||
|
// H4 : 18px / line 1.35
|
||||||
|
'typographyHeadingH4' => [
|
||||||
|
'font-size' => '1.125rem',
|
||||||
|
'line-height' => '1.35',
|
||||||
|
],
|
||||||
|
'h4Margin' => $sp('0', '0', '0.75rem', '0'),
|
||||||
|
|
||||||
|
// H5 : 16px / line 1.40
|
||||||
|
'typographyHeadingH5' => [
|
||||||
|
'font-size' => '1rem',
|
||||||
|
'line-height' => '1.40',
|
||||||
|
],
|
||||||
|
'h5Margin' => $sp('0', '0', '0.5rem', '0'),
|
||||||
|
|
||||||
|
// H6 : 14px / line 1.40
|
||||||
|
'typographyHeadingH6' => [
|
||||||
|
'font-size' => '0.875rem',
|
||||||
|
'line-height' => '1.40',
|
||||||
|
],
|
||||||
|
'h6Margin' => $sp('0', '0', '0.5rem', '0'),
|
||||||
|
|
||||||
|
// Focus outline (a11y, WCAG 2.1)
|
||||||
|
'focusOutline' => '2px solid var(--color-info)',
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- BUTTON : default + primary + secondary defaults ---- */
|
||||||
|
'button' => [
|
||||||
|
'typography' => [
|
||||||
|
'font-family' => 'Inter',
|
||||||
|
'font-weight' => '600',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sizeDefaultPadding' => $sp('0.75rem', '1.5rem', '0.75rem', '1.5rem'),
|
||||||
|
'sizeSmPadding' => $sp('0.5rem', '1rem', '0.5rem', '1rem'),
|
||||||
|
'sizeMdPadding' => $sp('0.75rem', '1.5rem', '0.75rem', '1.5rem'),
|
||||||
|
'sizeLgPadding' => $sp('1rem', '2rem', '1rem', '2rem'),
|
||||||
|
|
||||||
|
// Primary button = navy bg, white text, gold hover (managed via element classes)
|
||||||
|
'primaryBackground' => $color('var(--color-brand-navy)'),
|
||||||
|
'primaryTypography' => [
|
||||||
|
'color' => $color('var(--color-text-on-dark)'),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Secondary button = gold bg, navy text
|
||||||
|
'secondaryBackground' => $color('var(--color-brand-gold)'),
|
||||||
|
'secondaryTypography' => [
|
||||||
|
'color' => $color('var(--color-brand-navy)'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- SECTION : default padding (8-pt grid) ---- */
|
||||||
|
'section' => [
|
||||||
|
'padding' => $sp('5rem', '4rem', '5rem', '4rem'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- CONTAINER : max-width 1280 ---- */
|
||||||
|
'container' => [
|
||||||
|
'widthMax' => '1280px',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
update_option('bricks_theme_styles', $styles);
|
||||||
|
$report[] = "bricks_theme_styles[asterion] = full payload (" . strlen(wp_json_encode($styles)) . " bytes)";
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------
|
||||||
|
3. Done
|
||||||
|
--------------------------------------------------------------- */
|
||||||
|
$report[] = "";
|
||||||
|
$report[] = "Reload the Bricks builder (Ctrl+R) — you should see :";
|
||||||
|
$report[] = " - Body in Inter 16px navy";
|
||||||
|
$report[] = " - H1-H6 in Inter 700 navy with proper sizes";
|
||||||
|
$report[] = " - Links in gold underlined";
|
||||||
|
$report[] = " - Sections with 5rem top/bottom padding";
|
||||||
|
$report[] = " - postTypes : page, post, case_study, scenario";
|
||||||
|
|
||||||
|
wp_die('<pre style="font:12px/1.5 monospace;padding:2rem;background:#0B1F3A;color:#E0C892;">'
|
||||||
|
. esc_html(implode("\n", $report))
|
||||||
|
. '</pre>', 'Asterion theme style applied');
|
||||||
|
});
|
||||||
304
tools/av-bricks-rest-bridge.php
Normal file
304
tools/av-bricks-rest-bridge.php
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Bricks REST Bridge
|
||||||
|
* Description: Exposes Bricks Builder data (templates CPT + page/post/CPT meta keys) over the WP REST API so the bricks-mcp-server (sabiertas) can read/write Bricks content. Idempotent — safe to keep installed.
|
||||||
|
*
|
||||||
|
* Why this is needed :
|
||||||
|
* - Bricks registers the bricks_template CPT WITHOUT show_in_rest, so /wp/v2/bricks_template returns 404.
|
||||||
|
* - The Bricks element JSON lives in post_meta keys (_bricks_page_content_2 etc.) that aren't exposed by default.
|
||||||
|
* - register_post_meta() with show_in_rest=true is the canonical way to expose them; meta is then read/written via /wp/v2/<post_type>/<id> with the `meta` field.
|
||||||
|
*
|
||||||
|
* Auth :
|
||||||
|
* - Read : public (REST default for show_in_rest)
|
||||||
|
* - Write : edit_posts capability (=> the WP Application Password user must have at least Editor role)
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
1) Force show_in_rest=true on bricks_template + our CPTs.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
add_filter('register_post_type_args', function ($args, $post_type) {
|
||||||
|
|
||||||
|
$needs_rest = ['bricks_template', 'case_study', 'scenario'];
|
||||||
|
|
||||||
|
if (! in_array($post_type, $needs_rest, true)) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$args['show_in_rest'] = true;
|
||||||
|
$args['rest_base'] = $post_type === 'bricks_template' ? 'bricks_template' : ($post_type . 's');
|
||||||
|
$args['rest_controller_class'] = 'WP_REST_Posts_Controller';
|
||||||
|
$args['supports'] = array_unique(array_merge(
|
||||||
|
$args['supports'] ?? [],
|
||||||
|
['title', 'editor', 'author', 'custom-fields'] // custom-fields needed so /wp/v2/<type> exposes the `meta` field
|
||||||
|
));
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}, 10, 2);
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
2) Register Bricks meta keys for REST on every relevant post type.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
add_action('init', function () {
|
||||||
|
|
||||||
|
// Post types that may carry Bricks content
|
||||||
|
$post_types = ['page', 'post', 'bricks_template', 'case_study', 'scenario'];
|
||||||
|
|
||||||
|
// Bricks meta keys we want exposed (read+write) over REST
|
||||||
|
// Keys come from wp-content/themes/bricks/functions.php
|
||||||
|
$bricks_meta = [
|
||||||
|
'_bricks_page_content_2' => 'string', // The element JSON (the main payload)
|
||||||
|
'_bricks_page_css_2' => 'string',
|
||||||
|
'_bricks_page_header_2' => 'string',
|
||||||
|
'_bricks_page_footer_2' => 'string',
|
||||||
|
'_bricks_page_settings' => 'string',
|
||||||
|
'_bricks_template_type' => 'string',
|
||||||
|
'_bricks_template_settings' => 'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
$auth_cb = function () {
|
||||||
|
return current_user_can('edit_posts');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pass-through sanitize : keep the JSON string verbatim. Default sanitize for
|
||||||
|
// type=string strips slashes / escapes which corrupts our JSON payload.
|
||||||
|
$sanitize_passthrough = function ($value) { return $value; };
|
||||||
|
|
||||||
|
foreach ($post_types as $type) {
|
||||||
|
foreach ($bricks_meta as $meta_key => $meta_type) {
|
||||||
|
register_post_meta($type, $meta_key, [
|
||||||
|
'type' => $meta_type,
|
||||||
|
'single' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'auth_callback' => $auth_cb,
|
||||||
|
'sanitize_callback' => $sanitize_passthrough,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30); // priority 30 — after both Bricks (10-25) and our CPTs registration (10)
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
3) Permit also unknown _bricks_* meta keys (e.g. _bricks_snapshot_<ts>)
|
||||||
|
so MCP snapshots work without us pre-registering each key.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
add_filter('is_protected_meta', function ($protected, $meta_key, $meta_type) {
|
||||||
|
if ($meta_type !== 'post') return $protected;
|
||||||
|
if (strpos($meta_key, '_bricks_snapshot_') === 0) {
|
||||||
|
return false; // expose snapshot meta in REST despite the leading underscore
|
||||||
|
}
|
||||||
|
return $protected;
|
||||||
|
}, 10, 3);
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
4) AUTO-MIRROR : the bricks-mcp-server writes element JSON to
|
||||||
|
_bricks_page_content_2, but Bricks reads HEADER templates from
|
||||||
|
_bricks_page_header_2 and FOOTER templates from _bricks_page_footer_2.
|
||||||
|
Mirror the content into the correct key whenever a bricks_template
|
||||||
|
with type=header/footer is saved.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
/* DUAL-FORMAT META READ : the MCP server writes Bricks element data as a JSON
|
||||||
|
string into post_meta. Bricks's renderer expects a PHP array. We keep the JSON
|
||||||
|
string format in DB (so REST reads it back fine for the MCP's Json.parse), and
|
||||||
|
decode on the fly when Bricks reads via get_post_meta (non-REST context).
|
||||||
|
Result : same value, two views — JSON for the MCP, array for the renderer. */
|
||||||
|
add_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10, 5);
|
||||||
|
|
||||||
|
function av_decode_bricks_meta_for_renderer($value, $post_id, $meta_key, $single, $meta_type) {
|
||||||
|
// Only post-type meta concerns us
|
||||||
|
if ($meta_type !== 'post') return $value;
|
||||||
|
|
||||||
|
// Only Bricks element content keys
|
||||||
|
$content_keys = ['_bricks_page_content_2', '_bricks_page_header_2', '_bricks_page_footer_2'];
|
||||||
|
if (! in_array($meta_key, $content_keys, true)) return $value;
|
||||||
|
|
||||||
|
// In REST context, leave the raw string for the MCP server
|
||||||
|
if (defined('REST_REQUEST') && REST_REQUEST) return $value;
|
||||||
|
|
||||||
|
// Bricks renderer context : fetch the raw stored value (bypassing this filter
|
||||||
|
// via remove_filter, then re-add) and decode JSON → array
|
||||||
|
remove_filter('get_post_metadata', __FUNCTION__, 10);
|
||||||
|
$raw = get_post_meta($post_id, $meta_key, $single);
|
||||||
|
add_filter('get_post_metadata', __FUNCTION__, 10, 5);
|
||||||
|
|
||||||
|
if (! is_string($raw)) return $value;
|
||||||
|
$trim = trim($raw);
|
||||||
|
if ($trim === '' || $trim === '[]') return $value;
|
||||||
|
if ($trim[0] !== '[' && $trim[0] !== '{') return $value;
|
||||||
|
|
||||||
|
$decoded = json_decode($raw, true);
|
||||||
|
if (! is_array($decoded)) return $value;
|
||||||
|
|
||||||
|
// WP's get_metadata_raw() convention for filter return values :
|
||||||
|
// - When filter returns array AND $single=true → WP returns $check[0]
|
||||||
|
// - When filter returns array AND $single=false → WP returns $check as-is
|
||||||
|
// Both cases need a list wrapper around our decoded value so WP's
|
||||||
|
// single-mode auto-indexing yields the decoded array (not the section).
|
||||||
|
return [$decoded];
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('updated_post_meta', 'av_mirror_bricks_template_content', 10, 4);
|
||||||
|
add_action('added_post_meta', 'av_mirror_bricks_template_content', 10, 4);
|
||||||
|
|
||||||
|
function av_mirror_bricks_template_content($meta_id, $post_id, $meta_key, $meta_value) {
|
||||||
|
// Trigger 1 : content updated → mirror to header/footer if type already known
|
||||||
|
// Trigger 2 : type updated → re-mirror existing content (fixes the order-of-writes
|
||||||
|
// on REST create_template which writes content before type)
|
||||||
|
if (! in_array($meta_key, ['_bricks_page_content_2', '_bricks_template_type'], true)) return;
|
||||||
|
if (get_post_type($post_id) !== 'bricks_template') return;
|
||||||
|
|
||||||
|
// Reuse the av_decode filter logic by reading via get_post_meta (which now
|
||||||
|
// returns array) when the value is JSON, or by direct decoding here.
|
||||||
|
if ($meta_key === '_bricks_page_content_2') {
|
||||||
|
$type = get_post_meta($post_id, '_bricks_template_type', true);
|
||||||
|
$content = is_string($meta_value) ? $meta_value : '';
|
||||||
|
} else {
|
||||||
|
// type just updated → fetch raw content (bypass our filter to get string)
|
||||||
|
$type = is_string($meta_value) ? $meta_value : '';
|
||||||
|
remove_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10);
|
||||||
|
$content = get_post_meta($post_id, '_bricks_page_content_2', true);
|
||||||
|
add_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mirror_key = match ($type) {
|
||||||
|
'header' => '_bricks_page_header_2',
|
||||||
|
'footer' => '_bricks_page_footer_2',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (! $mirror_key) return;
|
||||||
|
|
||||||
|
// Bricks stores element JSON as a PHP-serialized array. Decode JSON string to array first.
|
||||||
|
$value = is_string($content) ? json_decode($content, true) : $content;
|
||||||
|
if (! is_array($value)) return;
|
||||||
|
|
||||||
|
// Bypass register_post_meta string-coercion by writing a serialized blob directly via $wpdb.
|
||||||
|
// This is what Bricks's own builder save does — the value lands as a serialized PHP array,
|
||||||
|
// which `get_post_meta($id, $key, true)` auto-unserializes back to array on read.
|
||||||
|
global $wpdb;
|
||||||
|
$serialized = maybe_serialize($value);
|
||||||
|
$existing = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
||||||
|
$post_id, $mirror_key
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->postmeta,
|
||||||
|
['meta_value' => $serialized],
|
||||||
|
['meta_id' => $existing],
|
||||||
|
['%s'], ['%d']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$wpdb->insert(
|
||||||
|
$wpdb->postmeta,
|
||||||
|
['post_id' => $post_id, 'meta_key' => $mirror_key, 'meta_value' => $serialized],
|
||||||
|
['%d', '%s', '%s']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
wp_cache_delete($post_id, 'post_meta');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
5) BACKFILL : trigger a manual mirror via URL.
|
||||||
|
Useful one-shot for templates created before this plugin was installed.
|
||||||
|
Visit /?av_mirror_template=ID
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
/* One-shot : ensure all Bricks content meta is stored as JSON STRING in DB.
|
||||||
|
- PHP-serialized arrays (legacy from earlier hook) → re-encode to JSON
|
||||||
|
- JSON strings → leave alone
|
||||||
|
This is the canonical storage format. The av_decode filter handles
|
||||||
|
the array-view for Bricks's renderer at read time.
|
||||||
|
/?av_fix_all_bricks_content=1 */
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_fix_all_bricks_content'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
"SELECT meta_id, post_id, meta_key, meta_value
|
||||||
|
FROM {$wpdb->postmeta}
|
||||||
|
WHERE meta_key IN ('_bricks_page_content_2','_bricks_page_header_2','_bricks_page_footer_2')
|
||||||
|
AND LENGTH(meta_value) > 4"
|
||||||
|
);
|
||||||
|
|
||||||
|
$report = ['Normalizing Bricks content meta to JSON strings...'];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$val = $r->meta_value;
|
||||||
|
$first = $val[0] ?? '';
|
||||||
|
|
||||||
|
if ($first === '[' || $first === '{') {
|
||||||
|
// Already JSON string : skip
|
||||||
|
$report[] = " OK meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (JSON)";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialized PHP array → unserialize, JSON-encode
|
||||||
|
if (is_serialized($val)) {
|
||||||
|
$array = unserialize($val);
|
||||||
|
if (is_array($array)) {
|
||||||
|
$json = wp_json_encode($array);
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->postmeta,
|
||||||
|
['meta_value' => $json],
|
||||||
|
['meta_id' => $r->meta_id],
|
||||||
|
['%s'], ['%d']
|
||||||
|
);
|
||||||
|
wp_cache_delete($r->post_id, 'post_meta');
|
||||||
|
$report[] = " CONV meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (" . count($array) . " elements, " . strlen($json) . " bytes)";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$report[] = " SKIP meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (unknown format)";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_mirror_template'])) return;
|
||||||
|
// localhost-only debug — no auth required (we're on http://localhost:10004)
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$id = (int) $_GET['av_mirror_template'];
|
||||||
|
$content = get_post_meta($id, '_bricks_page_content_2', true);
|
||||||
|
$type = get_post_meta($id, '_bricks_template_type', true);
|
||||||
|
|
||||||
|
if (! is_string($content) && ! is_array($content)) wp_die('no content meta');
|
||||||
|
|
||||||
|
$value = is_string($content) ? json_decode($content, true) : $content;
|
||||||
|
if (! is_array($value)) wp_die('content is not an array');
|
||||||
|
|
||||||
|
$mirror_key = match ($type) {
|
||||||
|
'header' => '_bricks_page_header_2',
|
||||||
|
'footer' => '_bricks_page_footer_2',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (! $mirror_key) wp_die("no mirror needed for type=$type");
|
||||||
|
|
||||||
|
// Write directly to DB as a serialized PHP array (the format Bricks's renderer expects)
|
||||||
|
global $wpdb;
|
||||||
|
$serialized = maybe_serialize($value);
|
||||||
|
$existing = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
||||||
|
$id, $mirror_key
|
||||||
|
));
|
||||||
|
if ($existing) {
|
||||||
|
$wpdb->update($wpdb->postmeta, ['meta_value' => $serialized], ['meta_id' => $existing], ['%s'], ['%d']);
|
||||||
|
} else {
|
||||||
|
$wpdb->insert($wpdb->postmeta, ['post_id' => $id, 'meta_key' => $mirror_key, 'meta_value' => $serialized], ['%d', '%s', '%s']);
|
||||||
|
}
|
||||||
|
wp_cache_delete($id, 'post_meta');
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
$after = get_post_meta($id, $mirror_key, true);
|
||||||
|
$is_array = is_array($after);
|
||||||
|
|
||||||
|
wp_die('<pre style="font:12px/1.5 monospace;padding:2rem;background:#0B1F3A;color:#E0C892;">'
|
||||||
|
. esc_html("Mirror done : template #$id ($type)\n _bricks_page_content_2 -> $mirror_key\n " . count($value) . " elements\n stored as " . ($is_array ? 'array (correct)' : 'string (WRONG)'))
|
||||||
|
. '</pre>', 'Mirror done');
|
||||||
|
});
|
||||||
48
tools/av-cleanup-orphans.php
Normal file
48
tools/av-cleanup-orphans.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Clean WPML translation orphans (one-shot)
|
||||||
|
* /?av_cleanup_orphans=1 → removes "Secteurs" duplicate FR pages + empty icl_translations rows
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (empty($_GET['av_cleanup_orphans'])) return;
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 1) Find all "Secteurs" posts (any status) — they are duplicate FR pages
|
||||||
|
$dups = $wpdb->get_results(
|
||||||
|
"SELECT ID, post_title, post_name, post_status FROM {$wpdb->posts}
|
||||||
|
WHERE post_type='page' AND (post_title='Secteurs' OR post_name LIKE 'secteurs%')"
|
||||||
|
);
|
||||||
|
echo "Found " . count($dups) . " 'Secteurs' page(s):\n";
|
||||||
|
foreach ($dups as $d) {
|
||||||
|
echo " #{$d->ID} title=\"{$d->post_title}\" slug={$d->post_name} status={$d->post_status}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Force-delete all of them (will be recreated cleanly)
|
||||||
|
$deleted = 0;
|
||||||
|
foreach ($dups as $d) {
|
||||||
|
wp_delete_post($d->ID, true);
|
||||||
|
$wpdb->delete($wpdb->prefix . 'icl_translations', ['element_id' => $d->ID, 'element_type' => 'post_page']);
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
echo "Deleted $deleted posts + their icl_translations rows.\n\n";
|
||||||
|
|
||||||
|
// 3) Remove empty icl_translations rows for trid=39 (Industries hub)
|
||||||
|
$empty_rows = $wpdb->query(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=39 AND (element_id IS NULL OR element_id = 0)"
|
||||||
|
);
|
||||||
|
echo "Deleted $empty_rows empty FR rows for trid=39.\n\n";
|
||||||
|
|
||||||
|
// 4) Verify current state of trid=39
|
||||||
|
$rows = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}icl_translations WHERE trid=39");
|
||||||
|
echo "Current icl_translations for trid=39:\n";
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
echo " translation_id={$r->translation_id} element_id={$r->element_id} lang={$r->language_code}\n";
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
});
|
||||||
99
tools/av-create-wp-menu.php
Normal file
99
tools/av-create-wp-menu.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Create WP Menu (one-shot, idempotent)
|
||||||
|
* Description: /?av_create_menu=NAME → creates/updates a WP nav menu and returns its term_id.
|
||||||
|
*
|
||||||
|
* Predefined menus :
|
||||||
|
* /?av_create_menu=header-main → 5 items : Solutions / Industries / Technology / Why Asterion / Insights
|
||||||
|
* /?av_create_menu=footer-nav → Solutions / Industries / Technology pages
|
||||||
|
* /?av_create_menu=footer-co → About / Customers / Partners / Insights
|
||||||
|
* /?av_create_menu=footer-legal → Privacy / Terms / Cookies / Imprint
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_create_menu'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/nav-menu.php';
|
||||||
|
|
||||||
|
$name = sanitize_text_field($_GET['av_create_menu']);
|
||||||
|
|
||||||
|
$blueprints = [
|
||||||
|
'header-main' => [
|
||||||
|
'label' => 'Header — Main',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'Solutions', 'url' => '/solutions/'],
|
||||||
|
['title' => 'Industries', 'url' => '/industries/'],
|
||||||
|
['title' => 'Technology', 'url' => '/technology/'],
|
||||||
|
['title' => 'Why Asterion', 'url' => '/why-asterion/'],
|
||||||
|
['title' => 'Insights', 'url' => '/insights/'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'footer-nav' => [
|
||||||
|
'label' => 'Footer — Solutions',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'Solutions', 'url' => '/solutions/'],
|
||||||
|
['title' => 'Industries', 'url' => '/industries/'],
|
||||||
|
['title' => 'Technology', 'url' => '/technology/'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'footer-co' => [
|
||||||
|
'label' => 'Footer — Company',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'About', 'url' => '/about/'],
|
||||||
|
['title' => 'Customers', 'url' => '/customers/'],
|
||||||
|
['title' => 'Partners', 'url' => '/partners/'],
|
||||||
|
['title' => 'Insights', 'url' => '/insights/'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'footer-legal' => [
|
||||||
|
'label' => 'Footer — Legal',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'Privacy', 'url' => '/privacy/'],
|
||||||
|
['title' => 'Terms', 'url' => '/terms/'],
|
||||||
|
['title' => 'Cookies', 'url' => '/cookies/'],
|
||||||
|
['title' => 'Imprint', 'url' => '/imprint/'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! isset($blueprints[$name])) {
|
||||||
|
wp_die('unknown menu blueprint : ' . esc_html($name) . '. Available : ' . implode(', ', array_keys($blueprints)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$bp = $blueprints[$name];
|
||||||
|
$label = $bp['label'];
|
||||||
|
|
||||||
|
// Idempotent : reuse existing menu if name matches
|
||||||
|
$menu = wp_get_nav_menu_object($label);
|
||||||
|
if (! $menu) {
|
||||||
|
$menu_id = wp_create_nav_menu($label);
|
||||||
|
if (is_wp_error($menu_id)) wp_die('create error : ' . $menu_id->get_error_message());
|
||||||
|
$menu = wp_get_nav_menu_object($menu_id);
|
||||||
|
} else {
|
||||||
|
// Wipe existing items to repush fresh
|
||||||
|
$existing = wp_get_nav_menu_items($menu->term_id);
|
||||||
|
foreach ($existing as $item) {
|
||||||
|
wp_delete_post($item->ID, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = ["Menu \"$label\" (term_id={$menu->term_id})"];
|
||||||
|
|
||||||
|
foreach ($bp['items'] as $i => $item) {
|
||||||
|
$item_id = wp_update_nav_menu_item($menu->term_id, 0, [
|
||||||
|
'menu-item-title' => $item['title'],
|
||||||
|
'menu-item-url' => $item['url'],
|
||||||
|
'menu-item-status' => 'publish',
|
||||||
|
'menu-item-type' => 'custom',
|
||||||
|
'menu-item-position' => $i + 1,
|
||||||
|
]);
|
||||||
|
$report[] = " + {$item['title']} → {$item['url']} (item_id=$item_id)";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
echo "\n\nUSE IN BRICKS : nav-menu element with menu = {$menu->term_id}";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
31
tools/av-debug-meta.php
Normal file
31
tools/av-debug-meta.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Debug raw meta value
|
||||||
|
* Description: /?av_raw_meta=ID&key=KEY → dumps the raw type and first chars of a post meta. No auth (localhost).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_raw_meta'])) return;
|
||||||
|
|
||||||
|
$id = (int) $_GET['av_raw_meta'];
|
||||||
|
$key = sanitize_text_field($_GET['key'] ?? '_bricks_page_header_2');
|
||||||
|
|
||||||
|
if (! preg_match('/^[a-zA-Z0-9_]{2,60}$/', $key)) wp_die('bad key');
|
||||||
|
|
||||||
|
$val = get_post_meta($id, $key, true);
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo "post=$id key=$key\n";
|
||||||
|
echo "type=" . gettype($val) . "\n";
|
||||||
|
if (is_string($val)) {
|
||||||
|
echo "len=" . strlen($val) . "\n";
|
||||||
|
echo "first 200: " . substr($val, 0, 200) . "\n";
|
||||||
|
} elseif (is_array($val)) {
|
||||||
|
echo "count=" . count($val) . "\n";
|
||||||
|
echo "json: " . substr(json_encode($val), 0, 500) . "\n";
|
||||||
|
} else {
|
||||||
|
echo "value: " . var_export($val, true) . "\n";
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
});
|
||||||
34
tools/av-dump-bricks.php
Normal file
34
tools/av-dump-bricks.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Dump Bricks Options (one-shot)
|
||||||
|
* Description: Visit /?av_dump_bricks=1 (admin only) → dumps bricks_theme_styles, bricks_color_palette, bricks_global_settings to JSON files in C:\ASTERION\GIT\WP2026\Bricks Config\.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! is_admin() && empty($_GET['av_dump_bricks'])) return;
|
||||||
|
if (! current_user_can('manage_options')) return;
|
||||||
|
if (empty($_GET['av_dump_bricks'])) return;
|
||||||
|
|
||||||
|
$out_dir = 'C:\\ASTERION\\GIT\\WP2026\\Bricks Config\\';
|
||||||
|
if (! is_dir($out_dir)) {
|
||||||
|
wp_die('Output dir not found: ' . esc_html($out_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'bricks_theme_styles' => 'theme-styles-current.json',
|
||||||
|
'bricks_color_palette' => 'color-palette-current.json',
|
||||||
|
'bricks_global_settings' => 'global-settings-current.json',
|
||||||
|
];
|
||||||
|
|
||||||
|
$report = [];
|
||||||
|
foreach ($options as $opt_key => $file_name) {
|
||||||
|
$value = get_option($opt_key, null);
|
||||||
|
$path = $out_dir . $file_name;
|
||||||
|
$json = wp_json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
file_put_contents($path, $json);
|
||||||
|
$report[] = $opt_key . ' → ' . $path . ' (' . strlen($json) . ' bytes)';
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_die('<pre>' . esc_html(implode("\n", $report)) . '</pre>', 'Dump done');
|
||||||
|
});
|
||||||
137
tools/av-fix-footer-headings.php
Normal file
137
tools/av-fix-footer-headings.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Align + style footer column sub-titles
|
||||||
|
* /?av_fix_footer_headings=1
|
||||||
|
*
|
||||||
|
* Standardizes the 4 footer column headings (Solutions / Company / Legal /
|
||||||
|
* Contact) with eyebrow-style typography : gold, uppercase, letter-spacing,
|
||||||
|
* consistent size. Tightens the brand heading. Aligns columns to flex-start
|
||||||
|
* so sub-titles sit at the same vertical baseline.
|
||||||
|
*
|
||||||
|
* Idempotent — re-runs are safe.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
CSS overrides : reset <ul> default padding-left on footer nav-menus
|
||||||
|
so links align with their column heading. Also restore tagline color
|
||||||
|
that was inheriting too-dark var(--color-text-tertiary).
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('wp_head', function () {
|
||||||
|
?>
|
||||||
|
<style id="av-footer-overrides">
|
||||||
|
/* Footer nav-menu : remove default <ul> padding-left so links align with
|
||||||
|
the column heading (otherwise they look indented by ~40px). */
|
||||||
|
#brx-footer .bricks-nav-menu {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
list-style: none !important;
|
||||||
|
}
|
||||||
|
#brx-footer .bricks-nav-menu li {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
list-style: none !important;
|
||||||
|
}
|
||||||
|
#brx-footer .bricks-nav-menu > li {
|
||||||
|
padding: 0.25rem 0 !important;
|
||||||
|
}
|
||||||
|
#brx-footer .bricks-nav-menu > li > a {
|
||||||
|
padding: 0 !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
/* Brand tagline : ensure it stays light/visible on the dark footer bg. */
|
||||||
|
#brxe-ftrtagln {
|
||||||
|
color: rgba(255, 255, 255, 0.65) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}, 999);
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (empty($_GET['av_fix_footer_headings'])) return;
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$eyebrow_typography = [
|
||||||
|
'color' => ['raw' => 'var(--color-brand-gold)'],
|
||||||
|
'font-size' => '0.75rem',
|
||||||
|
'font-weight' => '600',
|
||||||
|
'letter-spacing' => '0.1em',
|
||||||
|
'text-transform' => 'uppercase',
|
||||||
|
'line-height' => '1.4',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (['_bricks_page_content_2', '_bricks_page_footer_2'] as $key) {
|
||||||
|
$val = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
||||||
|
46, $key
|
||||||
|
));
|
||||||
|
if (! is_string($val) || $val === '') continue;
|
||||||
|
$is_json = ($val[0] === '[');
|
||||||
|
$elements = $is_json ? json_decode($val, true) : @unserialize($val);
|
||||||
|
if (! is_array($elements)) continue;
|
||||||
|
|
||||||
|
$changed = 0;
|
||||||
|
foreach ($elements as $i => $el) {
|
||||||
|
$id = $el['id'] ?? '';
|
||||||
|
|
||||||
|
// 1. Footer column sub-titles : eyebrow style
|
||||||
|
if (in_array($id, ['ftrh2', 'ftrh3', 'ftrh4', 'ftrh5'], true)) {
|
||||||
|
$elements[$i]['settings']['_typography'] = $eyebrow_typography;
|
||||||
|
$elements[$i]['settings']['_margin'] = ['top' => '0', 'right' => '0', 'bottom' => '0.5rem', 'left' => '0'];
|
||||||
|
$changed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Brand heading (ASTERION VR) — smaller so it doesn't dominate
|
||||||
|
if ($id === 'ftrbrnd') {
|
||||||
|
$elements[$i]['settings']['_typography'] = [
|
||||||
|
'color' => ['raw' => 'var(--color-text-on-dark)'],
|
||||||
|
'font-size' => '1.125rem',
|
||||||
|
'font-weight' => '600',
|
||||||
|
'letter-spacing' => '0.08em',
|
||||||
|
'line-height' => '1.4',
|
||||||
|
];
|
||||||
|
$elements[$i]['settings']['_margin'] = ['top' => '0', 'right' => '0', 'bottom' => '0.5rem', 'left' => '0'];
|
||||||
|
$changed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Brand tagline : muted, smaller
|
||||||
|
if ($id === 'ftrtagln') {
|
||||||
|
$elements[$i]['settings']['_typography'] = [
|
||||||
|
'color' => ['raw' => 'var(--color-text-tertiary)'],
|
||||||
|
'font-size' => '0.875rem',
|
||||||
|
'line-height' => '1.55',
|
||||||
|
];
|
||||||
|
$changed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Columns wrapper : align children to flex-start so column tops match
|
||||||
|
if ($id === 'ftrcols') {
|
||||||
|
$elements[$i]['settings']['_alignItems'] = 'flex-start';
|
||||||
|
$elements[$i]['settings']['_columnGap'] = '3rem';
|
||||||
|
$elements[$i]['settings']['_rowGap'] = '2.5rem';
|
||||||
|
$changed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Each column container : tight row gap between heading and content
|
||||||
|
if (in_array($id, ['ftrcol1', 'ftrcol2', 'ftrcol3', 'ftrcol4', 'ftrcol5'], true)) {
|
||||||
|
$elements[$i]['settings']['_rowGap'] = '0.5rem';
|
||||||
|
$changed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($changed > 0) {
|
||||||
|
$new = $is_json ? wp_json_encode($elements) : maybe_serialize($elements);
|
||||||
|
$wpdb->update($wpdb->postmeta, ['meta_value' => $new],
|
||||||
|
['post_id' => 46, 'meta_key' => $key], ['%s'], ['%d', '%s']);
|
||||||
|
wp_cache_delete(46, 'post_meta');
|
||||||
|
echo " $key : $changed element(s) patched\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Reload any page → footer sub-titles now eyebrow-styled and aligned.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
65
tools/av-fix-header-logo-link.php
Normal file
65
tools/av-fix-header-logo-link.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Make Header logo link to home (one-shot)
|
||||||
|
* /?av_fix_header_logo=1 → patch element 'hdrlgo' in Bricks template #25
|
||||||
|
* to add link.type=external, link.url=/ so clicking the logo reloads home.
|
||||||
|
*
|
||||||
|
* Stored in _bricks_page_header_2 (serialized PHP array), and also in
|
||||||
|
* _bricks_page_content_2 (JSON) thanks to the dual-format read filter in
|
||||||
|
* av-bricks-rest-bridge.php.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (empty($_GET['av_fix_header_logo'])) return;
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$patched = 0;
|
||||||
|
foreach (['_bricks_page_content_2', '_bricks_page_header_2'] as $key) {
|
||||||
|
$val = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
||||||
|
25, $key
|
||||||
|
));
|
||||||
|
if (! is_string($val) || $val === '') {
|
||||||
|
echo " $key : (empty, skip)\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_json = ($val[0] === '[');
|
||||||
|
$elements = $is_json ? json_decode($val, true) : @unserialize($val);
|
||||||
|
if (! is_array($elements)) {
|
||||||
|
echo " $key : (not parseable, skip)\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modified = false;
|
||||||
|
foreach ($elements as $i => $el) {
|
||||||
|
if (($el['id'] ?? '') === 'hdrlgo') {
|
||||||
|
$elements[$i]['settings']['link'] = [
|
||||||
|
'type' => 'external',
|
||||||
|
'url' => '/',
|
||||||
|
];
|
||||||
|
$modified = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($modified) {
|
||||||
|
$new = $is_json ? wp_json_encode($elements) : maybe_serialize($elements);
|
||||||
|
$wpdb->update($wpdb->postmeta, ['meta_value' => $new],
|
||||||
|
['post_id' => 25, 'meta_key' => $key], ['%s'], ['%d', '%s']);
|
||||||
|
wp_cache_delete(25, 'post_meta');
|
||||||
|
$patched++;
|
||||||
|
echo " $key : hdrlgo patched (link.url=/)\n";
|
||||||
|
} else {
|
||||||
|
echo " $key : hdrlgo not found\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. $patched meta key(s) patched.\n";
|
||||||
|
echo "Reload any page → click 'ASTERION VR' in header → should go to /.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
81
tools/av-fix-link-types.php
Normal file
81
tools/av-fix-link-types.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Migrate Bricks links from type:'internal'+url to type:'external'+url
|
||||||
|
* Description: /?av_fix_link_types=1
|
||||||
|
* Bricks's set_link_attributes() in base.php expects 'internal' to come with a numeric
|
||||||
|
* 'postId' (it ignores 'url' when type='internal'), while 'external' uses the 'url'
|
||||||
|
* string verbatim. Older session-3 templates were built with the wrong shape and lost
|
||||||
|
* their href at render time. This one-shot scans _bricks_page_content_2,
|
||||||
|
* _bricks_page_header_2, _bricks_page_footer_2 across all posts (pages + bricks_template),
|
||||||
|
* rewrites every link block where type==='internal' && isset(url), and writes back.
|
||||||
|
*
|
||||||
|
* Idempotent : already-correct links are skipped. Safe to re-run.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_fix_link_types'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$keys = ['_bricks_page_content_2', '_bricks_page_header_2', '_bricks_page_footer_2'];
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
"SELECT meta_id, post_id, meta_key, meta_value
|
||||||
|
FROM {$wpdb->postmeta}
|
||||||
|
WHERE meta_key IN ('" . implode("','", $keys) . "')"
|
||||||
|
);
|
||||||
|
|
||||||
|
$patched = 0;
|
||||||
|
$touched_posts = 0;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$val = $row->meta_value;
|
||||||
|
if (! is_string($val) || $val === '') continue;
|
||||||
|
|
||||||
|
$is_json = ($val[0] === '[');
|
||||||
|
$elements = null;
|
||||||
|
|
||||||
|
if ($is_json) {
|
||||||
|
$elements = json_decode($val, true);
|
||||||
|
} else {
|
||||||
|
// Mirror keys (_bricks_page_header_2, _bricks_page_footer_2) are stored as
|
||||||
|
// PHP-serialized arrays by av-bricks-rest-bridge → maybe_unserialize.
|
||||||
|
$maybe = @unserialize($val);
|
||||||
|
if (is_array($maybe)) $elements = $maybe;
|
||||||
|
}
|
||||||
|
if (! is_array($elements)) continue;
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
av_walk_fix_links($elements, $count);
|
||||||
|
if ($count === 0) continue;
|
||||||
|
|
||||||
|
// Write back in same format as it was stored
|
||||||
|
$new = $is_json ? wp_json_encode($elements) : maybe_serialize($elements);
|
||||||
|
$wpdb->update($wpdb->postmeta, ['meta_value' => $new], ['meta_id' => $row->meta_id], ['%s'], ['%d']);
|
||||||
|
wp_cache_delete($row->post_id, 'post_meta');
|
||||||
|
$patched += $count;
|
||||||
|
$touched_posts++;
|
||||||
|
$fmt = $is_json ? 'json' : 'serialized';
|
||||||
|
echo "post #{$row->post_id} ({$row->meta_key}, $fmt) : $count link(s) patched\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nTotal : $patched links across $touched_posts post(s) patched (type internal\u{2192}external).\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_walk_fix_links(&$elements, &$count) {
|
||||||
|
$walk = function (&$node) use (&$walk, &$count) {
|
||||||
|
if (! is_array($node)) return;
|
||||||
|
// Link block shape : has type and url, type wrongly set to 'internal' with a non-numeric url
|
||||||
|
if (isset($node['type']) && $node['type'] === 'internal' && isset($node['url']) && is_string($node['url']) && $node['url'] !== '') {
|
||||||
|
$node['type'] = 'external';
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
foreach ($node as &$value) {
|
||||||
|
if (is_array($value)) $walk($value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
foreach ($elements as &$el) $walk($el);
|
||||||
|
}
|
||||||
68
tools/av-fix-template-meta.php
Normal file
68
tools/av-fix-template-meta.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Fix Bricks Template meta (one-shot)
|
||||||
|
* Description: Visit /?av_fix_template_meta=ID&type=header (admin only) to set _bricks_template_type and global conditions for a Bricks template. Also versionable for re-runs.
|
||||||
|
*
|
||||||
|
* Usage examples :
|
||||||
|
* /?av_fix_template_meta=25&type=header → mark template 25 as Header type, condition = entire website
|
||||||
|
* /?av_fix_template_meta=26&type=footer → mark template 26 as Footer type, condition = entire website
|
||||||
|
* /?av_fix_template_meta=42&type=section → mark as section template (no auto-condition)
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
|
||||||
|
if (empty($_GET['av_fix_template_meta'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$template_id = (int) $_GET['av_fix_template_meta'];
|
||||||
|
$type = sanitize_text_field($_GET['type'] ?? 'content');
|
||||||
|
|
||||||
|
$valid_types = ['header', 'footer', 'section', 'content', 'popup', 'archive', 'single', 'search', 'error'];
|
||||||
|
if (! in_array($type, $valid_types, true)) {
|
||||||
|
wp_die("Invalid type: $type. Must be one of: " . implode(', ', $valid_types));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get_post_type($template_id) !== 'bricks_template') {
|
||||||
|
wp_die("Post $template_id is not a bricks_template");
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = [];
|
||||||
|
|
||||||
|
// 1. Set template type
|
||||||
|
update_post_meta($template_id, '_bricks_template_type', $type);
|
||||||
|
$report[] = "_bricks_template_type = $type";
|
||||||
|
|
||||||
|
// 2. Set conditions = entire website (for header/footer/archive/single/search/error templates)
|
||||||
|
// Bricks expects _bricks_template_settings with a 'templateConditions' array
|
||||||
|
// Each condition shape : ['main' => 'any'] applies on the entire site
|
||||||
|
if (in_array($type, ['header', 'footer', 'archive', 'single', 'search', 'error'], true)) {
|
||||||
|
$settings = get_post_meta($template_id, '_bricks_template_settings', true);
|
||||||
|
if (! is_array($settings)) $settings = [];
|
||||||
|
|
||||||
|
$settings['templateConditions'] = [
|
||||||
|
[
|
||||||
|
'id' => substr(md5((string) $template_id . microtime()), 0, 6),
|
||||||
|
'main' => 'any',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Header templates : sticky on top by default unless &sticky=0
|
||||||
|
if ($type === 'header' && ($_GET['sticky'] ?? '1') !== '0') {
|
||||||
|
$settings['headerSticky'] = true;
|
||||||
|
// headerStickyOnScroll switches from position:fixed (default brx-sticky)
|
||||||
|
// to position:sticky → header flows normally on load (no content overlap),
|
||||||
|
// sticks only once user starts scrolling.
|
||||||
|
$settings['headerStickyOnScroll'] = true;
|
||||||
|
$report[] = "headerSticky = true + headerStickyOnScroll = true (.brx-sticky.on-scroll)";
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta($template_id, '_bricks_template_settings', $settings);
|
||||||
|
$report[] = "_bricks_template_settings.templateConditions = [{main:any}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_die('<pre style="font:12px/1.5 monospace;padding:2rem;background:#0B1F3A;color:#E0C892;">'
|
||||||
|
. esc_html("Template #$template_id ($type) updated:\n " . implode("\n ", $report))
|
||||||
|
. '</pre>', 'Template meta fixed');
|
||||||
|
});
|
||||||
172
tools/av-generate-editorial.php
Normal file
172
tools/av-generate-editorial.php
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate editorial pages (one-shot)
|
||||||
|
* Description: /?av_generate_editorial=1 reads content-source/editorial-data.php
|
||||||
|
* and creates/updates 5 top-level pages : /why-asterion/, /about/, /customers/,
|
||||||
|
* /trust/, /partners/. No hub — each page is independent at the site root.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Register Editorial-specific FR strings into the shared dict.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, av_editorial_fr_dict());
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_editorial_fr_dict() {
|
||||||
|
return [
|
||||||
|
/* ============================ Why Asterion ============================ */
|
||||||
|
'Why Asterion' => 'Pourquoi Asterion',
|
||||||
|
'A platform built around the operator. Not the other way around.'
|
||||||
|
=> "Une plateforme construite autour de l'opérateur. Pas l'inverse.",
|
||||||
|
"Asterion VR was founded in 2016 to solve one specific problem: security forces need more repetition than the ranges, ammunition budgets, and exercise calendars allow. Ten years later, the platform we built does exactly that \u{2014} at every scale, in every environment, for every role."
|
||||||
|
=> "Asterion VR a été fondé en 2016 pour résoudre un problème précis : les forces de sécurité ont besoin de plus de répétition que ce que les stands, les budgets munitions et les calendriers d'exercice permettent. Dix ans plus tard, la plateforme que nous avons construite fait exactement cela \u{2014} à toute échelle, dans tout environnement, pour tout rôle.",
|
||||||
|
|
||||||
|
'Read our customer stories' => 'Lisez nos retours clients',
|
||||||
|
'Meet the team' => "Rencontrez l'équipe",
|
||||||
|
|
||||||
|
'What makes us different' => 'Ce qui nous différencie',
|
||||||
|
'Six commitments engineered into the platform.' => 'Six engagements gravés dans la plateforme.',
|
||||||
|
|
||||||
|
'French-engineered, EU-sovereign' => 'Conçu en France, souverain européen',
|
||||||
|
'Designed and produced in France. Software developed in our French studio. Operates fully offline. No data leaves your network. No foreign cloud dependency. No ITAR exposure. GDPR-native by default.'
|
||||||
|
=> "Conçu et produit en France. Logiciel développé dans notre studio français. Fonctionne entièrement hors ligne. Aucune donnée ne quitte votre réseau. Aucune dépendance à un cloud étranger. Aucune exposition ITAR. RGPD-natif par défaut.",
|
||||||
|
'Modular from day one' => 'Modulaire dès le premier jour',
|
||||||
|
'MY PROSERVE, FLEX, ACADEMY \u{2014} same software, same scenarios, same instructor tools, three deployment formats. Start solo. Add operators. Scale to a full academy. Your investment never resets.'
|
||||||
|
=> "MY PROSERVE, FLEX, ACADEMY \u{2014} même logiciel, mêmes scénarios, mêmes outils instructeur, trois formats de déploiement. Commencez seul. Ajoutez des opérateurs. Passez à l'académie complète. Votre investissement ne repart jamais à zéro.",
|
||||||
|
'Real-recoil weapon integration' => 'Intégration d\'armes à recul réel',
|
||||||
|
'Train with the weapon you carry. Our Universal Weapon Digitizer attaches to gas-blowback replicas, CO2 replicas, or real service weapons firing blanks. Recoil is real. Tracking survives recoil. Muscle memory transfers.'
|
||||||
|
=> "Entraînez-vous avec l'arme que vous portez. Notre Universal Weapon Digitizer s'attache aux répliques gas-blowback, CO2, ou aux vraies armes de service à blanc. Le recul est réel. Le tracking survit au recul. La mémoire musculaire transfère.",
|
||||||
|
'Behavior, not scripts' => 'Du comportement, pas du script',
|
||||||
|
'AI civilians and adversaries with vision, hearing, and configurable personality. Same starting conditions, different outcomes. Trainees train against decisions, not pattern recognition.'
|
||||||
|
=> "Civils et adversaires IA avec vision, ouïe et personnalité configurable. Mêmes conditions de départ, issues différentes. Les stagiaires s'entraînent contre des décisions, pas la reconnaissance de patterns.",
|
||||||
|
'Instructor-first, not trainee-first' => 'Instructeur d\'abord, stagiaire ensuite',
|
||||||
|
'Most VR products are built for the headset wearer. We built ours for the instructor \u{2014} because the instructor is the one who turns immersion into learning.'
|
||||||
|
=> "La plupart des produits VR sont construits pour celui qui porte le casque. Nous avons construit le nôtre pour l'instructeur \u{2014} car c'est l'instructeur qui transforme l'immersion en apprentissage.",
|
||||||
|
'AAR redefined' => 'L\'AAR redéfini',
|
||||||
|
'Frame-level replay. Per-trainee statistics. Unit-level dashboards. Comparable across time. Exportable to your reporting systems. Every drill ends measurable.'
|
||||||
|
=> "Rejeu image par image. Statistiques par stagiaire. Tableaux de bord par unité. Comparable dans le temps. Exportable vers vos systèmes de reporting. Chaque exercice se termine mesurable.",
|
||||||
|
|
||||||
|
'Where we come from' => "D'où nous venons",
|
||||||
|
'The thread of Ariadne.' => "Le fil d'Ariane.",
|
||||||
|
"Asterion is the original Greek name of the Minotaur \u{2014} the creature that lived at the heart of Daedalus's labyrinth. He was confronted by Theseus, who survived by training, intelligence, and the thread of Ariadne. The name we carry is a reminder: in the labyrinth of contemporary security operations, training is the thread that brings operators home."
|
||||||
|
=> "Asterion est le nom grec d'origine du Minotaure \u{2014} la créature qui vivait au c\u{153}ur du labyrinthe de Dédale. Il fut affronté par Thésée, qui survécut grâce à l'entraînement, l'intelligence et le fil d'Ariane. Le nom que nous portons est un rappel : dans le labyrinthe des opérations de sécurité contemporaines, l'entraînement est le fil qui ramène les opérateurs chez eux.",
|
||||||
|
|
||||||
|
'The numbers' => 'Les chiffres',
|
||||||
|
'Asterion VR in figures.' => 'Asterion VR en chiffres.',
|
||||||
|
|
||||||
|
/* ============================ About ============================ */
|
||||||
|
'About' => 'À propos',
|
||||||
|
'Built in France, for forces that train where it counts.'
|
||||||
|
=> "Construit en France, pour les forces qui s'entraînent là où ça compte.",
|
||||||
|
"Asterion VR was founded in 2016 by J\u{E9}r\u{F4}me Foucher and a small team convinced of one thing: that the gap between regulation and reality in security forces training would not close on its own."
|
||||||
|
=> "Asterion VR a été fondé en 2016 par Jérôme Foucher et une petite équipe convaincue d'une chose : que l'écart entre la réglementation et la réalité dans la formation des forces de sécurité ne se refermerait pas tout seul.",
|
||||||
|
|
||||||
|
'The conviction' => 'La conviction',
|
||||||
|
'In 2016, we looked at the numbers.' => 'En 2016, nous avons regardé les chiffres.',
|
||||||
|
'French police regulation required 90 cartridges per officer per year. The actual median was between 10 and 15. Tactical errors under stress increased by 40% compared to baseline conditions. 42% of intervention errors were linked to insufficient realistic preparation. The infrastructure to fix this \u{2014} saturated ranges, expensive live exercises, finite instructor time \u{2014} was not going to expand. Something had to change in the methodology. VR was the obvious answer. But the VR available in 2016 was built for entertainment. The recoil was fake. The civilians were on rails. The instructor saw what the trainee saw. The data was nowhere. We started over.'
|
||||||
|
=> "La réglementation police française exigeait 90 cartouches par agent et par an. La médiane réelle était entre 10 et 15. Les erreurs tactiques sous stress augmentaient de 40% par rapport aux conditions de base. 42% des erreurs d'intervention étaient liées à une préparation réaliste insuffisante. L'infrastructure pour y remédier \u{2014} stands saturés, exercices réels coûteux, temps instructeur limité \u{2014} n'allait pas s'étendre. Quelque chose devait changer dans la méthode. La VR était la réponse évidente. Mais la VR disponible en 2016 était construite pour le divertissement. Le recul était factice. Les civils sur rails. L'instructeur voyait ce que voyait le stagiaire. Les données nulle part. Nous avons recommencé.",
|
||||||
|
|
||||||
|
'Ten years of one focus' => 'Dix ans, une seule mission',
|
||||||
|
'Security forces. Nothing else.' => 'Forces de sécurité. Rien d\'autre.',
|
||||||
|
'Asterion has never built a single product for entertainment, education, or industry. Our entire roadmap, from day one, has been the training of security forces. Every iteration of our weapon tracker, every line of code in our AI engine, every environment in our scenario library was specified by serving or former operators.'
|
||||||
|
=> "Asterion n'a jamais construit le moindre produit pour le divertissement, l'éducation ou l'industrie. Notre feuille de route entière, dès le premier jour, c'est la formation des forces de sécurité. Chaque itération de notre tracker d'arme, chaque ligne de code de notre moteur IA, chaque environnement de notre bibliothèque de scénarios a été spécifié par des opérateurs en service ou anciens.",
|
||||||
|
|
||||||
|
'The team' => 'L\'équipe',
|
||||||
|
'Engineers, operators, designers.' => 'Ingénieurs, opérateurs, designers.',
|
||||||
|
|
||||||
|
'Asterion in your network' => 'Asterion dans votre réseau',
|
||||||
|
'Members and partners.' => 'Membres et partenaires.',
|
||||||
|
'We are members of the French defense innovation cluster, the French Tech ecosystem, and active partners with HTC VIVE Business and Unreal Engine programmes for defense and public sector applications. We work with a network of distributors and integrators across Europe, the Middle East, and Asia.'
|
||||||
|
=> "Nous sommes membres du cluster français d'innovation défense, de l'écosystème French Tech, et partenaires actifs des programmes HTC VIVE Business et Unreal Engine pour les applications défense et secteur public. Nous travaillons avec un réseau de distributeurs et d'intégrateurs en Europe, au Moyen-Orient et en Asie.",
|
||||||
|
|
||||||
|
/* ============================ Customers ============================ */
|
||||||
|
'Customers & Case Studies' => 'Clients & retours d\'expérience',
|
||||||
|
'Customers' => 'Clients',
|
||||||
|
'Real deployments. Measured results.' => 'Déploiements réels. Résultats mesurés.',
|
||||||
|
'PROSERVE is in active service with police, gendarmerie, and tactical units across Europe. Some are public references. Some operate under nondisclosure agreements. The case studies below show \u{2014} under their own names, with their own metrics, in their own words \u{2014} how they train with us.'
|
||||||
|
=> "PROSERVE est en service actif auprès de polices, gendarmeries et unités tactiques à travers l'Europe. Certaines sont des références publiques. Certaines opèrent sous accord de confidentialité. Les retours d'expérience ci-dessous montrent \u{2014} sous leur nom, avec leurs propres métriques, dans leurs propres mots \u{2014} comment ils s'entraînent avec nous.",
|
||||||
|
|
||||||
|
'Become the next case study' => 'Devenez le prochain retour d\'expérience',
|
||||||
|
|
||||||
|
'Featured case studies' => 'Retours d\'expérience phares',
|
||||||
|
'Sample deployments \u{2014} to be detailed in /customers/[slug]/.'
|
||||||
|
=> "Déploiements types \u{2014} à détailler dans /customers/[slug]/.",
|
||||||
|
'National police academy, France' => 'École nationale de police, France',
|
||||||
|
'Continuous marksmanship training across 12 stations. 1500 officers trained in the first 18 months. Per-officer median annual cartridge count rose from 14 to 117 (live + simulated). Tactical-error rate on benchmark drill dropped 35%.'
|
||||||
|
=> "Entraînement tir continu sur 12 stations. 1500 agents formés dans les 18 premiers mois. Le nombre médian annuel de cartouches par agent est passé de 14 à 117 (réel + simulé). Le taux d'erreur tactique sur drill de référence a baissé de 35%.",
|
||||||
|
'Regional gendarmerie tactical unit' => 'Unité tactique de gendarmerie régionale',
|
||||||
|
'PROSERVE FLEX deployed for forward training during multi-week field rotations. Hostage-rescue drill repetition multiplied by 8 over 6 months. Reaction time under contact improved 22% on benchmark drill.'
|
||||||
|
=> "PROSERVE FLEX déployé pour entraînement avancé pendant des rotations terrain pluri-hebdomadaires. Répétition du drill de libération d'otages multipliée par 8 sur 6 mois. Temps de réaction sous contact amélioré de 22% sur le drill de référence.",
|
||||||
|
'Municipal police, large city' => 'Police municipale, grande ville',
|
||||||
|
'MY PROSERVE rolled out to 24 stations. Self-reported training confidence improved 41%. Continuous-training compliance rose from 38% to 92% within 12 months.'
|
||||||
|
=> "MY PROSERVE déployé sur 24 postes. La confiance en formation auto-déclarée s'est améliorée de 41%. La conformité à la formation continue est passée de 38% à 92% en 12 mois.",
|
||||||
|
'Firefighter regional training centre' => 'Centre régional de formation des sapeurs-pompiers',
|
||||||
|
'PROSERVE ACADEMY-FLEX hybrid for hazmat and confined-space rescue. Annual exercise volume tripled. Recruit retention improved due to better preparation for live exercise.'
|
||||||
|
=> "Hybride PROSERVE ACADEMY-FLEX pour interventions matières dangereuses et espaces confinés. Volume annuel d'exercice triplé. Rétention des recrues améliorée grâce à une meilleure préparation aux exercices réels.",
|
||||||
|
|
||||||
|
'Reference partners' => 'Partenaires de référence',
|
||||||
|
'If your unit is interested in becoming a public reference.' => 'Si votre unité souhaite devenir une référence publique.',
|
||||||
|
'We work with reference partners on a long-term basis. In exchange for the right to publish your story (with the level of detail you authorize), we offer extended support, priority on custom scenarios, and active involvement in our product roadmap. Reach out to discuss.'
|
||||||
|
=> "Nous travaillons avec les partenaires de référence sur le long terme. En échange du droit de publier votre histoire (au niveau de détail que vous autorisez), nous offrons un support étendu, la priorité sur les scénarios sur mesure et une implication active dans notre feuille de route produit. Contactez-nous pour en discuter.",
|
||||||
|
|
||||||
|
/* ============================ Trust ============================ */
|
||||||
|
'Trust & Compliance' => 'Confiance & conformité',
|
||||||
|
'Trust by engineering, not by claim.' => 'La confiance par l\'ingénierie, pas par l\'affirmation.',
|
||||||
|
'Asterion VR is engineered for the trust requirements of public security forces. Sovereignty, data protection, and operational autonomy are designed into the platform \u{2014} not bolted on for compliance.'
|
||||||
|
=> "Asterion VR est conçu pour les exigences de confiance des forces de sécurité publiques. Souveraineté, protection des données et autonomie opérationnelle sont intégrées à la plateforme \u{2014} pas plaquées pour la conformité.",
|
||||||
|
'Request a Trust Pack' => 'Demander un Trust Pack',
|
||||||
|
|
||||||
|
'Sovereignty' => 'Souveraineté',
|
||||||
|
'Designed European, supported European.' => 'Conçu européen, supporté européen.',
|
||||||
|
'Data protection' => 'Protection des données',
|
||||||
|
'Your data never leaves your network.' => 'Vos données ne quittent jamais votre réseau.',
|
||||||
|
'Operational autonomy' => 'Autonomie opérationnelle',
|
||||||
|
'Train without an internet connection.' => 'Entraînez-vous sans connexion internet.',
|
||||||
|
'Cyber posture' => 'Posture cyber',
|
||||||
|
'Hardened for institutional environments.' => 'Durci pour les environnements institutionnels.',
|
||||||
|
'Warranty & support' => 'Garantie & support',
|
||||||
|
'Perpetual licenses. Predictable support.' => 'Licences perpétuelles. Support prévisible.',
|
||||||
|
|
||||||
|
/* ============================ Partners ============================ */
|
||||||
|
'Partners' => 'Partenaires',
|
||||||
|
'Distribute PROSERVE in your region.' => 'Distribuez PROSERVE dans votre région.',
|
||||||
|
'Asterion partners with established defense, security, and integrator companies across Europe, the Middle East, Asia, and Latin America. Our partner network combines local presence with our French engineering and studio. We are looking for new partners in selected regions.'
|
||||||
|
=> "Asterion s'associe à des entreprises établies dans la défense, la sécurité et l'intégration en Europe, au Moyen-Orient, en Asie et en Amérique latine. Notre réseau de partenaires combine présence locale, ingénierie et studio français. Nous recherchons de nouveaux partenaires dans des régions sélectionnées.",
|
||||||
|
|
||||||
|
'Become a partner — apply' => 'Devenir partenaire — postulez',
|
||||||
|
'Why partner with Asterion' => 'Pourquoi être partenaire d\'Asterion',
|
||||||
|
'A complete commercial framework.' => 'Un cadre commercial complet.',
|
||||||
|
'What we look for' => 'Ce que nous recherchons',
|
||||||
|
'Selection criteria.' => 'Critères de sélection.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_editorial'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$data_file = 'C:\\ASTERION\\GIT\\WP2026\\content-source\\editorial-data.php';
|
||||||
|
if (! file_exists($data_file)) wp_die("data file not found: $data_file");
|
||||||
|
|
||||||
|
$editorial = include $data_file;
|
||||||
|
if (! is_array($editorial)) wp_die('editorial data not array');
|
||||||
|
|
||||||
|
$report = ["Rebuilding editorial pages (top-level, no hub)..."];
|
||||||
|
|
||||||
|
// 5 top-level detail pages : why-asterion, about, customers, trust, partners
|
||||||
|
foreach ($editorial as $slug => $d) {
|
||||||
|
$page = av_upsert_detail_page($slug, '', $d); // empty parent_path → top-level
|
||||||
|
$report[] = " $slug → page #{$page['id']} ({$page['action']}) — {$page['element_count']} elements (json={$page['json_len']}, stored={$page['stored_len']})";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
547
tools/av-generate-forms.php
Normal file
547
tools/av-generate-forms.php
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate conversion forms pages (one-shot)
|
||||||
|
* Description: /?av_generate_forms=1 reads content-source/forms-data.php and creates
|
||||||
|
* 4 top-level pages : /request-demo/, /request-te-kit/, /request-quote/, /contact/
|
||||||
|
* with Bricks form element + 2-col layout (form + promise list).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
FR strings dict for forms
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, [
|
||||||
|
// Page titles + taglines + hero leads
|
||||||
|
'Request a Demo' => 'Réserver une démo',
|
||||||
|
'See PROSERVE in action.' => 'Voir PROSERVE en action.',
|
||||||
|
'A live demo, calibrated to your role and your unit. We respond within 48 hours.'
|
||||||
|
=> 'Une démo en direct, calibrée à votre rôle et votre unité. Nous répondons sous 48 heures.',
|
||||||
|
'Request a T&E Kit' => 'Demander un kit T&E',
|
||||||
|
'Try PROSERVE in your own facility, with your own people.'
|
||||||
|
=> 'Essayez PROSERVE dans vos locaux, avec votre équipe.',
|
||||||
|
"A demo only goes so far. The Test & Evaluation Kit programme lends a fully configured PROSERVE case to qualifying organizations for 5 to 10 days, on site, with our team's support."
|
||||||
|
=> "Une démo a ses limites. Le programme Test & Evaluation prête un coffre PROSERVE entièrement configuré aux organisations éligibles pour 5 à 10 jours, sur site, avec le support de notre équipe.",
|
||||||
|
'Request a Quote' => 'Demander un devis',
|
||||||
|
'From specification to deployment plan.' => 'De la spécification au plan de déploiement.',
|
||||||
|
'Tell us what you need. We send back a detailed quote \u{2014} including hardware, software, options, training, support, and deployment plan \u{2014} within 5 working days.'
|
||||||
|
=> "Dites-nous ce dont vous avez besoin. Nous renvoyons un devis détaillé \u{2014} matériel, logiciel, options, formation, support et plan de déploiement \u{2014} sous 5 jours ouvrés.",
|
||||||
|
'Contact' => 'Contact',
|
||||||
|
'How can we help?' => 'Comment pouvons-nous aider ?',
|
||||||
|
"If you know what you're looking for, the three CTAs below take you directly there. For everything else, the form at the bottom reaches our office."
|
||||||
|
=> "Si vous savez ce que vous cherchez, les trois CTA ci-dessous vous mènent directement. Pour tout le reste, le formulaire en bas atteint notre bureau.",
|
||||||
|
|
||||||
|
// Promise titles
|
||||||
|
'What you receive' => 'Ce que vous recevez',
|
||||||
|
'Other inquiries' => 'Autres demandes',
|
||||||
|
|
||||||
|
// Promise items (Demo)
|
||||||
|
"A 30-minute video call with a member of our team \u{2014} typically J\u{E9}r\u{F4}me, our founder, or a senior product manager."
|
||||||
|
=> "Un appel vidéo de 30 minutes avec un membre de notre équipe \u{2014} typiquement Jérôme, notre fondateur, ou un product manager senior.",
|
||||||
|
'A live walkthrough of the configuration most relevant to your needs (MY / FLEX / ACADEMY).'
|
||||||
|
=> 'Une démonstration en direct de la configuration la plus pertinente (MY / FLEX / ACADEMY).',
|
||||||
|
'A scenario demonstration matched to your segment (police, special forces, military, fire).'
|
||||||
|
=> 'Une démonstration de scénario adaptée à votre segment (police, forces spéciales, armée, pompiers).',
|
||||||
|
'Honest answers to your technical and procurement questions.'
|
||||||
|
=> 'Réponses honnêtes à vos questions techniques et d\'achat.',
|
||||||
|
'A follow-up email summarizing the discussion, with the right resources for your evaluation.'
|
||||||
|
=> "Un email de suivi résumant l'échange, avec les bonnes ressources pour votre évaluation.",
|
||||||
|
|
||||||
|
// Promise items (T&E Kit)
|
||||||
|
'A complete PROSERVE FLEX or MY PROSERVE configuration delivered to your facility.'
|
||||||
|
=> 'Une configuration complète PROSERVE FLEX ou MY PROSERVE livrée dans vos locaux.',
|
||||||
|
'On-site setup and instructor onboarding by our team (1-2 days).'
|
||||||
|
=> "Installation sur site et formation instructeur par notre équipe (1-2 jours).",
|
||||||
|
'Remote technical support throughout the loan period.'
|
||||||
|
=> 'Support technique à distance pendant toute la durée du prêt.',
|
||||||
|
'Up to 10 days of unrestricted use by your operators and instructors.'
|
||||||
|
=> 'Jusqu\'à 10 jours d\'utilisation sans restriction par vos opérateurs et instructeurs.',
|
||||||
|
'A debrief session at the end to capture observations and answer technical questions.'
|
||||||
|
=> 'Une séance de débrief en fin de période pour recueillir les observations et répondre aux questions techniques.',
|
||||||
|
|
||||||
|
// Promise items (Quote)
|
||||||
|
'A detailed quote (PDF, suitable for procurement records)'
|
||||||
|
=> 'Un devis détaillé (PDF, conforme aux exigences d\'archivage d\'achat)',
|
||||||
|
'A deployment plan with milestones and timing'
|
||||||
|
=> 'Un plan de déploiement avec jalons et calendrier',
|
||||||
|
'A 30-minute call with our commercial team to walk through the quote'
|
||||||
|
=> 'Un appel de 30 minutes avec notre équipe commerciale pour parcourir le devis',
|
||||||
|
'References for an introduction call with comparable customers (subject to NDA)'
|
||||||
|
=> 'Des références pour un appel d\'introduction avec des clients comparables (sous NDA)',
|
||||||
|
|
||||||
|
// Promise items (Contact)
|
||||||
|
'Press & media — press@asterionvr.com (response within 24 hours)'
|
||||||
|
=> 'Presse & médias — press@asterionvr.com (réponse sous 24 heures)',
|
||||||
|
'Partnerships & distributors — partners@asterionvr.com'
|
||||||
|
=> 'Partenariats & distributeurs — partners@asterionvr.com',
|
||||||
|
'Careers — careers@asterionvr.com' => 'Carrières — careers@asterionvr.com',
|
||||||
|
'General — hello@asterionvr.com' => 'Général — hello@asterionvr.com',
|
||||||
|
"Office \u{2014} Asterion VR \u{B7} 9 rue de la Mareb\u{E9}audi\u{E8}re \u{B7} 35760 Montgermont \u{B7} France \u{B7} +33 6 86 94 33 51"
|
||||||
|
=> "Bureau \u{2014} Asterion VR \u{B7} 9 rue de la Marebaudière \u{B7} 35760 Montgermont \u{B7} France \u{B7} +33 6 86 94 33 51",
|
||||||
|
|
||||||
|
// Reassurance lines
|
||||||
|
'We respond within 48 hours. We never share your information. No newsletter sign-up unless you ask.'
|
||||||
|
=> 'Nous répondons sous 48 heures. Nous ne partageons jamais vos informations. Pas d\'inscription newsletter sauf demande.',
|
||||||
|
'Limited slots per quarter. We respond within 5 working days with a yes-or-no on the loan, and a proposed schedule if eligible.'
|
||||||
|
=> "Places limitées par trimestre. Nous répondons sous 5 jours ouvrés avec un oui/non sur le prêt et un calendrier proposé si éligible.",
|
||||||
|
'We respond within 5 working days. Detailed quote suitable for procurement records.'
|
||||||
|
=> 'Nous répondons sous 5 jours ouvrés. Devis détaillé conforme aux exigences d\'achat.',
|
||||||
|
'We respond within 48 hours on weekdays.' => 'Nous répondons sous 48 heures en jours ouvrés.',
|
||||||
|
|
||||||
|
// Submit labels
|
||||||
|
'Schedule the demo' => 'Réserver la démo',
|
||||||
|
'Apply for a T&E Kit' => 'Postuler pour un kit T&E',
|
||||||
|
'Request the quote' => 'Demander le devis',
|
||||||
|
'Send the message' => 'Envoyer le message',
|
||||||
|
|
||||||
|
// Form field labels (shared across forms)
|
||||||
|
'Full name' => 'Nom complet',
|
||||||
|
'Organization' => 'Organisation',
|
||||||
|
'Job title' => 'Fonction',
|
||||||
|
'Job title / role' => 'Fonction / rôle',
|
||||||
|
'Country' => 'Pays',
|
||||||
|
'Email' => 'Email',
|
||||||
|
'Segment' => 'Segment',
|
||||||
|
'Estimated number of trainees in your unit' => 'Nombre estimé de stagiaires dans votre unité',
|
||||||
|
'Preferred timeframe' => 'Délai préféré',
|
||||||
|
'Message — anything specific you want us to demonstrate'
|
||||||
|
=> 'Message — éléments spécifiques à démontrer',
|
||||||
|
'Message' => 'Message',
|
||||||
|
'Address of evaluation facility' => 'Adresse du site d\'évaluation',
|
||||||
|
'Number of trainees who would participate in evaluation'
|
||||||
|
=> 'Nombre de stagiaires participant à l\'évaluation',
|
||||||
|
'Procurement timeline' => 'Calendrier d\'achat',
|
||||||
|
'Indicative budget range' => 'Fourchette budgétaire indicative',
|
||||||
|
'Authorization status' => 'Statut d\'autorisation',
|
||||||
|
'Procurement vehicle' => 'Véhicule d\'achat',
|
||||||
|
'Configuration desired' => 'Configuration souhaitée',
|
||||||
|
'Number of units / packs / seats' => 'Nombre d\'unités / packs / postes',
|
||||||
|
'Options' => 'Options',
|
||||||
|
'Deployment location and timeline' => 'Lieu et calendrier de déploiement',
|
||||||
|
'Administrative constraints (open tender deadline, cybersecurity review process, etc.)'
|
||||||
|
=> 'Contraintes administratives (échéance d\'appel d\'offres, processus de revue cyber, etc.)',
|
||||||
|
'Subject' => 'Sujet',
|
||||||
|
|
||||||
|
// Select options
|
||||||
|
'Police' => 'Police',
|
||||||
|
'Special Forces' => 'Forces spéciales',
|
||||||
|
'Military' => 'Armée',
|
||||||
|
'Firefighters' => 'Sapeurs-pompiers',
|
||||||
|
'Other' => 'Autre',
|
||||||
|
'This week' => 'Cette semaine',
|
||||||
|
'Next week' => 'La semaine prochaine',
|
||||||
|
'Next month' => 'Le mois prochain',
|
||||||
|
'No rush' => 'Pas pressé',
|
||||||
|
'3 months' => '3 mois',
|
||||||
|
'6 months' => '6 mois',
|
||||||
|
'12 months' => '12 mois',
|
||||||
|
'18+ months' => '18+ mois',
|
||||||
|
'Cleared' => 'Validé',
|
||||||
|
'Pending' => 'En attente',
|
||||||
|
'Non-applicable' => 'Non applicable',
|
||||||
|
'Direct purchase' => 'Achat direct',
|
||||||
|
'Open tender' => 'Appel d\'offres ouvert',
|
||||||
|
'Framework contract' => 'Marché-cadre',
|
||||||
|
|
||||||
|
// FAQ (Demo)
|
||||||
|
'Will there be a sales pitch?' => 'Y aura-t-il un pitch commercial ?',
|
||||||
|
"No. The first call is technical and pedagogical. We assume you'll evaluate three or four solutions before deciding \u{2014} we want to give you the information you need to compare honestly."
|
||||||
|
=> "Non. Le premier appel est technique et pédagogique. Nous partons du principe que vous évaluerez trois ou quatre solutions avant de décider \u{2014} nous voulons vous donner les informations nécessaires pour comparer honnêtement.",
|
||||||
|
'Can I bring colleagues?' => 'Puis-je amener des collègues ?',
|
||||||
|
'Of course. The more roles in the room (training officer, IT, finance, command), the more useful the call.'
|
||||||
|
=> "Bien sûr. Plus il y a de rôles dans la pièce (responsable formation, IT, finance, commandement), plus l'appel est utile.",
|
||||||
|
'In what language?' => 'En quelle langue ?',
|
||||||
|
'EN, FR. Other languages on a per-request basis.'
|
||||||
|
=> 'EN, FR. Autres langues sur demande.',
|
||||||
|
|
||||||
|
// Eligibility (T&E Kit)
|
||||||
|
'Eligibility' => 'Éligibilité',
|
||||||
|
'The T&E Kit programme is reserved for organizations with a clear procurement intent and the capacity to run meaningful evaluation drills. We accept applications from:'
|
||||||
|
=> "Le programme T&E Kit est réservé aux organisations avec une intention d'achat claire et la capacité à conduire des exercices d'évaluation significatifs. Nous acceptons les candidatures de :",
|
||||||
|
'National and regional police, gendarmerie, and security forces'
|
||||||
|
=> 'Polices nationales et régionales, gendarmerie et forces de sécurité',
|
||||||
|
'Special forces and tactical units' => 'Forces spéciales et unités tactiques',
|
||||||
|
'Military training centres' => 'Centres de formation militaire',
|
||||||
|
'Firefighter and emergency services training schools'
|
||||||
|
=> 'Écoles de formation des sapeurs-pompiers et services d\'urgence',
|
||||||
|
'Approved distributors and integrators bidding on identified projects'
|
||||||
|
=> "Distributeurs et intégrateurs agréés répondant à des projets identifiés",
|
||||||
|
|
||||||
|
// Shortcuts (Contact)
|
||||||
|
'Request a Demo (response within 48 hours)'
|
||||||
|
=> 'Réserver une démo (réponse sous 48 heures)',
|
||||||
|
'Request a T&E Kit (response within 5 working days)'
|
||||||
|
=> 'Demander un kit T&E (réponse sous 5 jours ouvrés)',
|
||||||
|
'Request a Quote (response within 5 working days)'
|
||||||
|
=> 'Demander un devis (réponse sous 5 jours ouvrés)',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_forms'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$data_file = 'C:\\ASTERION\\GIT\\WP2026\\content-source\\forms-data.php';
|
||||||
|
if (! file_exists($data_file)) wp_die("data file not found: $data_file");
|
||||||
|
|
||||||
|
$forms = include $data_file;
|
||||||
|
if (! is_array($forms)) wp_die('forms data not array');
|
||||||
|
|
||||||
|
$report = ["Rebuilding 4 conversion forms pages..."];
|
||||||
|
|
||||||
|
foreach ($forms as $slug => $d) {
|
||||||
|
// Custom page builder for forms (hero + 2-col + form + extras + closer)
|
||||||
|
$elements = av_build_form_page_elements($slug, $d);
|
||||||
|
|
||||||
|
// Ensure page exists top-level
|
||||||
|
$existing = get_page_by_path($slug);
|
||||||
|
if ($existing) {
|
||||||
|
wp_update_post([
|
||||||
|
'ID' => $existing->ID, 'post_title' => $d['title'],
|
||||||
|
'post_status' => 'publish', 'post_parent' => 0,
|
||||||
|
]);
|
||||||
|
$page_id = $existing->ID;
|
||||||
|
$action = 'updated';
|
||||||
|
} else {
|
||||||
|
$page_id = wp_insert_post([
|
||||||
|
'post_type' => 'page', 'post_title' => $d['title'],
|
||||||
|
'post_name' => $slug, 'post_parent' => 0,
|
||||||
|
'post_status' => 'publish', 'post_content' => '',
|
||||||
|
]);
|
||||||
|
$action = 'created';
|
||||||
|
}
|
||||||
|
|
||||||
|
$write = av_write_bricks_page_elements($page_id, $elements);
|
||||||
|
$report[] = " $slug → page #$page_id ($action) — " . count($elements)
|
||||||
|
. " elements (json={$write['json_len']}, stored={$write['stored_len']})";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
FORM PAGE BUILDER
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_build_form_page_elements($slug, $d) {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// 1. HERO
|
||||||
|
$els = array_merge($els, av_build_hero($slug, $d));
|
||||||
|
|
||||||
|
// 2. 2-COLUMN SECTION : form left, promise right
|
||||||
|
$els = array_merge($els, av_build_form_two_col($slug, $d));
|
||||||
|
|
||||||
|
// 3. Optional: shortcuts (Contact page)
|
||||||
|
if (! empty($d['shortcuts'])) {
|
||||||
|
$els = array_merge($els, av_build_shortcuts_section($slug, $d['shortcuts']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Optional: eligibility (T&E Kit)
|
||||||
|
if (! empty($d['eligibility'])) {
|
||||||
|
$els = array_merge($els, av_build_list_section($slug, 'elig', [
|
||||||
|
'eyebrow' => $d['eligibility']['title'] ?? 'Eligibility',
|
||||||
|
'title' => '',
|
||||||
|
'lead' => $d['eligibility']['lead'] ?? '',
|
||||||
|
'items' => $d['eligibility']['items'] ?? [],
|
||||||
|
], 'var(--color-bg-default)'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Optional: FAQ (Demo)
|
||||||
|
if (! empty($d['faq'])) {
|
||||||
|
$els = array_merge($els, av_build_faq_section($slug, $d['faq']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. CLOSER CTA
|
||||||
|
$els = array_merge($els, av_build_closer_cta($slug));
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_build_form_two_col($slug, $d) {
|
||||||
|
$sid = "f{$slug}sc"; // section
|
||||||
|
$oct = "f{$slug}oc"; // outer container
|
||||||
|
$col1 = "f{$slug}c1"; // form column
|
||||||
|
$col2 = "f{$slug}c2"; // promise column
|
||||||
|
$frm = "f{$slug}fm"; // form element
|
||||||
|
$reas = "f{$slug}rs"; // reassurance text
|
||||||
|
$peyb = "f{$slug}pe"; // promise eyebrow
|
||||||
|
$plst = "f{$slug}pl"; // promise list container
|
||||||
|
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
$els[] = [
|
||||||
|
'id' => $sid, 'name' => 'section', 'parent' => 0, 'children' => [$oct],
|
||||||
|
'settings' => array_merge(av_section_padding_responsive(), [
|
||||||
|
'_background' => ['color' => av_color('var(--color-bg-default)')],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $oct, 'name' => 'container', 'parent' => $sid, 'children' => [$col1, $col2],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'row',
|
||||||
|
'_widthMax' => '1280px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_padding' => av_sp('0', '1.5rem', '0', '1.5rem'),
|
||||||
|
'_padding:mobile_landscape' => av_sp('0', '1rem', '0', '1rem'),
|
||||||
|
'_columnGap' => '3rem',
|
||||||
|
'_rowGap' => '2rem',
|
||||||
|
'_flexWrap' => 'wrap',
|
||||||
|
'_alignItems' => 'flex-start',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// LEFT COLUMN — form + reassurance
|
||||||
|
$els[] = [
|
||||||
|
'id' => $col1, 'name' => 'container', 'parent' => $oct, 'children' => [$frm, $reas],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '600px',
|
||||||
|
'_widthMin' => '0',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_flexBasis' => 'calc((100% - 3rem) / 2)',
|
||||||
|
'_flexBasis:tablet_portrait' => '100%',
|
||||||
|
'_flexGrow' => '1',
|
||||||
|
'_padding' => av_sp('0', '0', '0', '0'),
|
||||||
|
'_rowGap' => '1.5rem',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Bricks form element
|
||||||
|
$els[] = av_build_form_element($frm, $col1, $d);
|
||||||
|
|
||||||
|
// Reassurance line
|
||||||
|
if (! empty($d['reassurance'])) {
|
||||||
|
$els[] = av_text($reas, $col1, $d['reassurance'], 'var(--color-text-tertiary)', '0.875rem', '1.55');
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIGHT COLUMN — promise eyebrow + list
|
||||||
|
$els[] = [
|
||||||
|
'id' => $col2, 'name' => 'container', 'parent' => $oct, 'children' => [$peyb, $plst],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '480px',
|
||||||
|
'_widthMin' => '0',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_flexBasis' => 'calc((100% - 3rem) / 2)',
|
||||||
|
'_flexBasis:tablet_portrait' => '100%',
|
||||||
|
'_flexGrow' => '0',
|
||||||
|
'_padding' => av_sp('1rem', '1.75rem', '1.75rem', '1.75rem'),
|
||||||
|
'_background' => ['color' => av_color('var(--color-bg-subtle)')],
|
||||||
|
'_border' => [
|
||||||
|
'width' => av_sp('1px', '1px', '1px', '1px'),
|
||||||
|
'color' => av_color('var(--color-border-subtle)'),
|
||||||
|
'style' => 'solid',
|
||||||
|
'radius' => av_sp('4px', '4px', '4px', '4px'),
|
||||||
|
],
|
||||||
|
'_rowGap' => '0.75rem',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = av_heading($peyb, $col2, 'h3', $d['promise_title'] ?? 'What you receive');
|
||||||
|
|
||||||
|
// Promise list as text-basic items
|
||||||
|
$items = $d['promise_items'] ?? [];
|
||||||
|
$item_ids = [];
|
||||||
|
foreach ($items as $j => $_) $item_ids[] = "fp{$slug}i{$j}";
|
||||||
|
$els[] = [
|
||||||
|
'id' => $plst, 'name' => 'container', 'parent' => $col2, 'children' => $item_ids,
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_rowGap' => '0.6rem',
|
||||||
|
'_padding' => av_sp('0.5rem', '0', '0', '0'),
|
||||||
|
'_width' => '100%',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
foreach ($items as $j => $item) {
|
||||||
|
$els[] = [
|
||||||
|
'id' => "fp{$slug}i{$j}", 'name' => 'text-basic', 'parent' => $plst, 'children' => [],
|
||||||
|
'settings' => [
|
||||||
|
'text' => $item,
|
||||||
|
'_cssClasses' => 'av-list-item',
|
||||||
|
'_typography' => [
|
||||||
|
'color' => av_color('var(--color-text-secondary)'),
|
||||||
|
'font-size' => '0.9375rem',
|
||||||
|
'line-height' => '1.55',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a Bricks `form` element with the given fields, submit button, and
|
||||||
|
* email action (sends to hello@asterionvr.com). Fields config follows
|
||||||
|
* Bricks's repeater shape : each field is an associative array with 'id',
|
||||||
|
* 'type', 'label', 'required', and 'options' (for select).
|
||||||
|
*/
|
||||||
|
function av_build_form_element($id, $parent, $d) {
|
||||||
|
$bricks_fields = [];
|
||||||
|
foreach ($d['fields'] as $f) {
|
||||||
|
$field = [
|
||||||
|
'id' => substr(md5($id . $f['name']), 0, 6),
|
||||||
|
'type' => $f['type'],
|
||||||
|
'label' => $f['label'],
|
||||||
|
'name' => $f['name'],
|
||||||
|
];
|
||||||
|
if (! empty($f['required'])) $field['required'] = true;
|
||||||
|
if ($f['type'] === 'select' && ! empty($f['options'])) {
|
||||||
|
// Bricks expects a single string with "value : label\n" per line OR
|
||||||
|
// an array of {value, label}. We use a newline string for simplicity.
|
||||||
|
$opts_str = [];
|
||||||
|
foreach ($f['options'] as $opt) {
|
||||||
|
$opts_str[] = $opt === '' ? "" : "$opt : $opt";
|
||||||
|
}
|
||||||
|
$field['options'] = implode("\n", $opts_str);
|
||||||
|
}
|
||||||
|
$bricks_fields[] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $id, 'name' => 'form', 'parent' => $parent, 'children' => [],
|
||||||
|
'settings' => [
|
||||||
|
'fields' => $bricks_fields,
|
||||||
|
'submitButtonText' => $d['submit_label'] ?? 'Submit',
|
||||||
|
'actions' => ['email'],
|
||||||
|
'emailTo' => 'custom',
|
||||||
|
'email' => 'hello@asterionvr.com',
|
||||||
|
'fromEmail' => 'noreply@asterionvr.com',
|
||||||
|
'fromName' => 'Asterion VR website',
|
||||||
|
'replyToEmail' => true,
|
||||||
|
'subject' => 'Asterion VR — ' . ($d['title'] ?? 'New submission'),
|
||||||
|
'successMessage' => 'Thank you. We will get back to you shortly.',
|
||||||
|
'_typography' => [
|
||||||
|
'font-size' => '1rem',
|
||||||
|
],
|
||||||
|
'submitButtonStyle' => 'primary',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_build_shortcuts_section($slug, $shortcuts) {
|
||||||
|
$sid = "fs{$slug}sc";
|
||||||
|
$oct = "fs{$slug}oc";
|
||||||
|
$row = "fs{$slug}rw";
|
||||||
|
|
||||||
|
$btn_ids = [];
|
||||||
|
foreach ($shortcuts as $j => $sh) $btn_ids[] = "fs{$slug}b{$j}";
|
||||||
|
|
||||||
|
$els = [];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $sid, 'name' => 'section', 'parent' => 0, 'children' => [$oct],
|
||||||
|
'settings' => array_merge(av_section_padding_responsive('3rem', '3rem'), [
|
||||||
|
'_background' => ['color' => av_color('var(--color-bg-subtle)')],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $oct, 'name' => 'container', 'parent' => $sid, 'children' => [$row],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '1280px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_padding' => av_sp('0', '1.5rem', '0', '1.5rem'),
|
||||||
|
'_padding:mobile_landscape' => av_sp('0', '1rem', '0', '1rem'),
|
||||||
|
'_alignItems' => 'flex-start',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $row, 'name' => 'container', 'parent' => $oct, 'children' => $btn_ids,
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'row',
|
||||||
|
'_flexWrap' => 'wrap',
|
||||||
|
'_columnGap' => '1rem',
|
||||||
|
'_rowGap' => '1rem',
|
||||||
|
'_width' => '100%',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
foreach ($shortcuts as $j => $sh) {
|
||||||
|
$variant = ($sh['variant'] ?? 'primary') === 'primary' ? 'primary' : 'secondary';
|
||||||
|
$els[] = av_button("fs{$slug}b{$j}", $row, $sh['label'], $sh['href'], $variant);
|
||||||
|
}
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_build_faq_section($slug, $faq) {
|
||||||
|
$sid = "fq{$slug}sc";
|
||||||
|
$oct = "fq{$slug}oc";
|
||||||
|
$nrr = "fq{$slug}nr";
|
||||||
|
$hId = "fq{$slug}h2";
|
||||||
|
$lst = "fq{$slug}lt";
|
||||||
|
|
||||||
|
$item_ids = [];
|
||||||
|
foreach ($faq as $j => $_) $item_ids[] = "fq{$slug}i{$j}";
|
||||||
|
|
||||||
|
$els = [];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $sid, 'name' => 'section', 'parent' => 0, 'children' => [$oct],
|
||||||
|
'settings' => array_merge(av_section_padding_responsive(), [
|
||||||
|
'_background' => ['color' => av_color('var(--color-bg-default)')],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $oct, 'name' => 'container', 'parent' => $sid, 'children' => [$nrr],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '1280px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_padding' => av_sp('0', '1.5rem', '0', '1.5rem'),
|
||||||
|
'_padding:mobile_landscape' => av_sp('0', '1rem', '0', '1rem'),
|
||||||
|
'_alignItems' => 'flex-start',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $nrr, 'name' => 'container', 'parent' => $oct, 'children' => array_merge([$hId, $lst]),
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '800px',
|
||||||
|
'_widthMin' => '0',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_margin' => av_sp('0', 'auto', '0', '0'),
|
||||||
|
'_padding' => av_sp('0', '0', '0', '0'),
|
||||||
|
'_rowGap' => '1.5rem',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = av_heading($hId, $nrr, 'h2', 'FAQ');
|
||||||
|
|
||||||
|
$els[] = [
|
||||||
|
'id' => $lst, 'name' => 'container', 'parent' => $nrr, 'children' => $item_ids,
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_rowGap' => '1rem',
|
||||||
|
'_width' => '100%',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($faq as $j => $entry) {
|
||||||
|
$iid = "fq{$slug}i{$j}";
|
||||||
|
$qid = "fq{$slug}q{$j}";
|
||||||
|
$aid = "fq{$slug}a{$j}";
|
||||||
|
$els[] = [
|
||||||
|
'id' => $iid, 'name' => 'container', 'parent' => $lst, 'children' => [$qid, $aid],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_rowGap' => '0.5rem',
|
||||||
|
'_padding' => av_sp('1rem', '1.25rem', '1rem', '1.25rem'),
|
||||||
|
'_background' => ['color' => av_color('var(--color-bg-subtle)')],
|
||||||
|
'_border' => [
|
||||||
|
'width' => av_sp('1px', '1px', '1px', '1px'),
|
||||||
|
'color' => av_color('var(--color-border-subtle)'),
|
||||||
|
'style' => 'solid',
|
||||||
|
'radius' => av_sp('4px', '4px', '4px', '4px'),
|
||||||
|
],
|
||||||
|
'_width' => '100%',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = av_heading($qid, $iid, 'h3', $entry['q']);
|
||||||
|
$els[] = av_text($aid, $iid, $entry['a'], 'var(--color-text-secondary)', '1rem', '1.55');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
292
tools/av-generate-home.php
Normal file
292
tools/av-generate-home.php
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate Home page (one-shot)
|
||||||
|
* Description: /?av_generate_home=1 builds the home page (slug = "home", set as
|
||||||
|
* WP static front page). Custom-written from the PDF Strategie & Contenu D.1-D.2
|
||||||
|
* plus synthesis of Industries / Solutions / Technology / Editorial.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
FR strings dict for Home
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, [
|
||||||
|
// Hero
|
||||||
|
'Train like you operate.' => 'Entraînez-vous comme vous opérez.',
|
||||||
|
"PROSERVE\u{2122} is the modular XR training platform engineered for European security forces. Real recoil, autonomous AI, sovereign by design \u{2014} from solo officer to full academy."
|
||||||
|
=> "PROSERVE\u{2122} est la plateforme XR modulaire conçue pour les forces de sécurité européennes. Recul réel, IA autonome, souveraineté native \u{2014} de l'agent seul à l'académie complète.",
|
||||||
|
|
||||||
|
// 3 pillars row
|
||||||
|
'Three certainties' => 'Trois certitudes',
|
||||||
|
'A platform engineered around three commitments.' => 'Une plateforme construite autour de trois engagements.',
|
||||||
|
'Operational reality' => 'Réalité opérationnelle',
|
||||||
|
'Real recoil. Real weapons. Real consequences. AI civilians and adversaries that perceive and decide. No two drills play out the same way.'
|
||||||
|
=> "Recul réel. Vraies armes. Conséquences réelles. Civils et adversaires IA qui perçoivent et décident. Aucun exercice ne se joue deux fois pareil.",
|
||||||
|
'Modular by design' => 'Modulaire par conception',
|
||||||
|
'From 9 m\u{B2} solo to 900 m\u{B2} academy, same software, same scenarios, same instructor tools. Your investment scales with you.'
|
||||||
|
=> "De 9 m\u{B2} solo à 900 m\u{B2} académie, même logiciel, mêmes scénarios, mêmes outils instructeur. Votre investissement évolue avec vous.",
|
||||||
|
'Sovereign & secure' => 'Souverain & sécurisé',
|
||||||
|
'Designed and produced in France. Operates fully offline. No data leaves your network. GDPR-native by default.'
|
||||||
|
=> "Conçu et produit en France. Fonctionne entièrement hors ligne. Aucune donnée ne quitte votre réseau. RGPD-natif par défaut.",
|
||||||
|
|
||||||
|
// Industries teaser
|
||||||
|
'Who we serve' => 'Qui nous servons',
|
||||||
|
'Built for those who carry the weight.' => 'Conçu pour celles et ceux qui en portent le poids.',
|
||||||
|
|
||||||
|
// Solutions teaser
|
||||||
|
'The PROSERVE platform' => 'La plateforme PROSERVE',
|
||||||
|
'Pick the configuration that fits your unit.' => 'Choisissez la configuration adaptée à votre unité.',
|
||||||
|
|
||||||
|
// Numbers band
|
||||||
|
'By the numbers' => 'En chiffres',
|
||||||
|
'10 years of XR R&D focused on security forces.'
|
||||||
|
=> "10 ans de R&D XR consacrés aux forces de sécurité.",
|
||||||
|
'10 years' => '10 ans',
|
||||||
|
'XR R&D for security forces' => 'R&D XR pour les forces de sécurité',
|
||||||
|
'11 environments' => '11 environnements',
|
||||||
|
'Validated training contexts' => 'Contextes d\'entraînement validés',
|
||||||
|
'50+ scenarios' => '50+ scénarios',
|
||||||
|
'Updated four times a year' => 'Mis à jour quatre fois par an',
|
||||||
|
'GDPR-native' => 'RGPD-natif',
|
||||||
|
'Fully offline operation' => 'Fonctionnement entièrement hors ligne',
|
||||||
|
|
||||||
|
// Why Asterion teaser
|
||||||
|
'Why teams choose Asterion' => 'Pourquoi les équipes choisissent Asterion',
|
||||||
|
'A platform built around the operator. Not the other way around.'
|
||||||
|
=> "Une plateforme construite autour de l'opérateur. Pas l'inverse.",
|
||||||
|
"Founded in 2016 to solve one specific problem: security forces need more repetition than the ranges, ammunition budgets, and exercise calendars allow. Ten years later, our platform does exactly that \u{2014} at every scale, in every environment, for every role."
|
||||||
|
=> "Fondée en 2016 pour résoudre un problème précis : les forces de sécurité ont besoin de plus de répétition que ce que les stands, budgets munitions et calendriers d'exercice permettent. Dix ans plus tard, notre plateforme fait exactement cela \u{2014} à toute échelle, dans tout environnement, pour tout rôle.",
|
||||||
|
'Read the full story' => 'Lire l\'histoire complète',
|
||||||
|
|
||||||
|
// Customers teaser
|
||||||
|
'Trusted by units across Europe' => 'Choisi par des unités en Europe',
|
||||||
|
'Real deployments. Measured results.' => 'Déploiements réels. Résultats mesurés.',
|
||||||
|
"From national police academies to special tactical units, PROSERVE is in active service \u{2014} with measurable impact on training volume, tactical accuracy, and operator confidence."
|
||||||
|
=> "Des écoles nationales de police aux unités tactiques spéciales, PROSERVE est en service actif \u{2014} avec un impact mesurable sur le volume d'entraînement, la précision tactique et la confiance des opérateurs.",
|
||||||
|
'See customer stories' => 'Voir les retours clients',
|
||||||
|
|
||||||
|
// Final closer (separate from shared closer because we override copy)
|
||||||
|
'Ready to see PROSERVE in action?' => 'Prêt à voir PROSERVE en action ?',
|
||||||
|
'Schedule a tailored demo or request a Test & Evaluation Kit.'
|
||||||
|
=> 'Réservez une démo personnalisée ou demandez un kit Test & Evaluation.',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_home'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
// Ensure home page exists (slug 'home') AND is set as WP static front page
|
||||||
|
$home = get_page_by_path('home');
|
||||||
|
if (! $home) {
|
||||||
|
$home_id = wp_insert_post([
|
||||||
|
'post_type' => 'page', 'post_title' => 'Home',
|
||||||
|
'post_name' => 'home', 'post_parent' => 0,
|
||||||
|
'post_status' => 'publish', 'post_content' => '',
|
||||||
|
]);
|
||||||
|
$action = 'created';
|
||||||
|
} else {
|
||||||
|
$home_id = $home->ID;
|
||||||
|
$action = 'updated';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set as WP static front page
|
||||||
|
update_option('show_on_front', 'page');
|
||||||
|
update_option('page_on_front', $home_id);
|
||||||
|
|
||||||
|
$elements = av_build_home_elements();
|
||||||
|
$write = av_write_bricks_page_elements($home_id, $elements);
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo "Home page #$home_id ($action) — " . count($elements) . " elements (json={$write['json_len']}, stored={$write['stored_len']})\n";
|
||||||
|
echo "show_on_front = page, page_on_front = $home_id\n";
|
||||||
|
echo "Visit / to see the home.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
HOME PAGE BUILDER
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_build_home_elements() {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// 1. HERO — full bleed, large tagline
|
||||||
|
$els = array_merge($els, av_build_hero('home', [
|
||||||
|
'eyebrow' => '',
|
||||||
|
'tagline' => 'Train like you operate.',
|
||||||
|
'hero_lead' => "PROSERVE\u{2122} is the modular XR training platform engineered for European security forces. Real recoil, autonomous AI, sovereign by design \u{2014} from solo officer to full academy.",
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'Book a demo', 'href' => '/request-demo/', 'variant' => 'primary'],
|
||||||
|
['label' => 'Request a T&E Kit', 'href' => '/request-te-kit/', 'variant' => 'secondary'],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// 2. THREE PILLARS — 3-card grid (count===3 → 33% each, clean row of 3)
|
||||||
|
$els = array_merge($els, av_build_cards_section('homep', 0, [
|
||||||
|
'eyebrow' => 'Three certainties',
|
||||||
|
'title' => 'A platform engineered around three commitments.',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'Operational reality',
|
||||||
|
'body' => 'Real recoil. Real weapons. Real consequences. AI civilians and adversaries that perceive and decide. No two drills play out the same way.'],
|
||||||
|
['title' => 'Modular by design',
|
||||||
|
'body' => "From 9 m\u{B2} solo to 900 m\u{B2} academy, same software, same scenarios, same instructor tools. Your investment scales with you."],
|
||||||
|
['title' => 'Sovereign & secure',
|
||||||
|
'body' => 'Designed and produced in France. Operates fully offline. No data leaves your network. GDPR-native by default.'],
|
||||||
|
],
|
||||||
|
], 'var(--color-bg-subtle)'));
|
||||||
|
|
||||||
|
// 3. INDUSTRIES — 4 cards linking to /industries/*
|
||||||
|
$industries_cards = [
|
||||||
|
'eyebrow' => 'Who we serve',
|
||||||
|
'title' => 'Built for those who carry the weight.',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'Police', 'body' => "National & Municipal \u{2014} Police training, when the schedule won't bend."],
|
||||||
|
['title' => 'Special Forces', 'body' => "Tactical units \u{2014} Special forces train differently. Their simulation should, too."],
|
||||||
|
['title' => 'Military', 'body' => "Armed forces \u{2014} Military VR training. Sovereign. Modular. Field-deployable."],
|
||||||
|
['title' => 'Firefighters', 'body' => "Emergency services \u{2014} Train the worst day. Safely. Repeatedly."],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els = array_merge($els, av_build_cards_section('homei', 1, $industries_cards, 'var(--color-bg-default)'));
|
||||||
|
// Append "Learn more →" link in each industry card
|
||||||
|
$industry_slugs = ['police', 'special-forces', 'military', 'firefighters'];
|
||||||
|
foreach ($industry_slugs as $i => $slug) {
|
||||||
|
$kid = "khomeic1x{$i}";
|
||||||
|
$lid = "lnhomeic1x{$i}";
|
||||||
|
$els[] = av_text_link($lid, $kid, "Learn more \u{2192}", "/industries/$slug/");
|
||||||
|
foreach ($els as &$el) {
|
||||||
|
if (($el['id'] ?? '') === $kid) {
|
||||||
|
$el['children'][] = $lid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($el);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. SOLUTIONS — 4 cards linking to /solutions/*
|
||||||
|
$solutions_cards = [
|
||||||
|
'eyebrow' => 'The PROSERVE platform',
|
||||||
|
'title' => 'Pick the configuration that fits your unit.',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'MY PROSERVE', 'body' => "Solo training \u{2014} Your first range, in a mini-case."],
|
||||||
|
['title' => 'PROSERVE FLEX', 'body' => "Team training \u{B7} 1 to 5 \u{2014} Your team, in the same scenario, on any flat surface."],
|
||||||
|
['title' => 'PROSERVE ACADEMY', 'body' => "Unit training \u{B7} 1 to 16 \u{2014} The full training centre, in your facility."],
|
||||||
|
['title' => 'PROSERVE+', 'body' => "Customization & options \u{2014} When the catalog doesn't fit your mission."],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els = array_merge($els, av_build_cards_section('homes', 2, $solutions_cards, 'var(--color-bg-subtle)'));
|
||||||
|
$solution_slugs = ['my-proserve', 'proserve-flex', 'proserve-academy', 'customization'];
|
||||||
|
foreach ($solution_slugs as $i => $slug) {
|
||||||
|
$kid = "khomesc2x{$i}";
|
||||||
|
$lid = "lnhomesc2x{$i}";
|
||||||
|
$els[] = av_text_link($lid, $kid, "Learn more \u{2192}", "/solutions/$slug/");
|
||||||
|
foreach ($els as &$el) {
|
||||||
|
if (($el['id'] ?? '') === $kid) {
|
||||||
|
$el['children'][] = $lid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($el);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. NUMBERS BAND — 4-card grid stats
|
||||||
|
$els = array_merge($els, av_build_cards_section('homen', 3, [
|
||||||
|
'eyebrow' => 'By the numbers',
|
||||||
|
'title' => '10 years of XR R&D focused on security forces.',
|
||||||
|
'items' => [
|
||||||
|
['title' => '10 years', 'body' => 'XR R&D for security forces'],
|
||||||
|
['title' => '11 environments', 'body' => 'Validated training contexts'],
|
||||||
|
['title' => '50+ scenarios', 'body' => 'Updated four times a year'],
|
||||||
|
['title' => 'GDPR-native', 'body' => 'Fully offline operation'],
|
||||||
|
],
|
||||||
|
], 'var(--color-bg-default)'));
|
||||||
|
|
||||||
|
// 6. WHY ASTERION teaser — prose with CTA
|
||||||
|
$els = array_merge($els, av_build_cta_section('homew', 4, [
|
||||||
|
'eyebrow' => 'Why teams choose Asterion',
|
||||||
|
'title' => 'A platform built around the operator. Not the other way around.',
|
||||||
|
'body' => "Founded in 2016 to solve one specific problem: security forces need more repetition than the ranges, ammunition budgets, and exercise calendars allow. Ten years later, our platform does exactly that \u{2014} at every scale, in every environment, for every role.",
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'Read the full story', 'href' => '/why-asterion/', 'variant' => 'secondary'],
|
||||||
|
],
|
||||||
|
], 'var(--color-bg-subtle)'));
|
||||||
|
|
||||||
|
// 7. CUSTOMERS teaser
|
||||||
|
$els = array_merge($els, av_build_cta_section('homec', 5, [
|
||||||
|
'eyebrow' => 'Trusted by units across Europe',
|
||||||
|
'title' => 'Real deployments. Measured results.',
|
||||||
|
'body' => "From national police academies to special tactical units, PROSERVE is in active service \u{2014} with measurable impact on training volume, tactical accuracy, and operator confidence.",
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'See customer stories', 'href' => '/customers/', 'variant' => 'secondary'],
|
||||||
|
],
|
||||||
|
], 'var(--color-bg-default)'));
|
||||||
|
|
||||||
|
// 8. CLOSER CTA — full navy with custom copy
|
||||||
|
$els = array_merge($els, av_build_home_closer_cta());
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_build_home_closer_cta() {
|
||||||
|
$sid = "schmhc";
|
||||||
|
$oct = "ochmhc";
|
||||||
|
$nrr = "nrhmhc";
|
||||||
|
$eyb = "ebhmhc";
|
||||||
|
$hId = "h2hmhc";
|
||||||
|
$led = "ldhmhc";
|
||||||
|
$row = "rwhmhc";
|
||||||
|
$b1 = "b1hmhc";
|
||||||
|
$b2 = "b2hmhc";
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'id' => $sid, 'name' => 'section', 'parent' => 0, 'children' => [$oct],
|
||||||
|
'settings' => array_merge(av_section_padding_responsive(), [
|
||||||
|
'_background' => ['color' => av_color('var(--color-brand-navy)')],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => $oct, 'name' => 'container', 'parent' => $sid, 'children' => [$nrr],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '1280px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_padding' => av_sp('0', '1.5rem', '0', '1.5rem'),
|
||||||
|
'_padding:mobile_landscape' => av_sp('0', '1rem', '0', '1rem'),
|
||||||
|
'_alignItems' => 'flex-start',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => $nrr, 'name' => 'container', 'parent' => $oct, 'children' => [$eyb, $hId, $led, $row],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '800px',
|
||||||
|
'_widthMin' => '0',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_margin' => av_sp('0', 'auto', '0', '0'),
|
||||||
|
'_padding' => av_sp('0', '0', '0', '0'),
|
||||||
|
'_rowGap' => '1.25rem',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
av_eyebrow($eyb, $nrr, 'Ready to see PROSERVE in action?'),
|
||||||
|
av_heading($hId, $nrr, 'h2', 'Schedule a tailored demo or request a Test & Evaluation Kit.', 'var(--color-text-on-dark)'),
|
||||||
|
av_text($led, $nrr, "Book 30 minutes with our team. We'll bring the kit. You bring the doctrine.", 'var(--color-text-on-dark)', '1.125rem', '1.55'),
|
||||||
|
[
|
||||||
|
'id' => $row, 'name' => 'container', 'parent' => $nrr, 'children' => [$b1, $b2],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'row',
|
||||||
|
'_columnGap' => '1rem',
|
||||||
|
'_rowGap' => '1rem',
|
||||||
|
'_flexWrap' => 'wrap',
|
||||||
|
'_widthMin' => '0',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_padding' => av_sp('0.5rem', '0', '0', '0'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
av_button($b1, $row, 'Book a Demo', '/request-demo/', 'gold'),
|
||||||
|
av_button($b2, $row, 'Request a T&E Kit', '/request-te-kit/', 'outline-on-dark'),
|
||||||
|
];
|
||||||
|
}
|
||||||
149
tools/av-generate-industries.php
Normal file
149
tools/av-generate-industries.php
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate industry pages (one-shot)
|
||||||
|
* Description: /?av_generate_industries=1 reads content-source/industries-data.php
|
||||||
|
* and creates/updates : hub /industries/ + 4 child pages (police, special-forces,
|
||||||
|
* military, firefighters). Idempotent.
|
||||||
|
*
|
||||||
|
* Heavy lifting (builders + FR pipeline) lives in _av-bricks-shared.php.
|
||||||
|
* This file only owns : URL trigger + hub builder + FR strings for Industries.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Register Industries-specific FR strings into the shared dict.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, [
|
||||||
|
// Hub
|
||||||
|
'Industries' => 'Secteurs',
|
||||||
|
"Built for those who carry the weight."
|
||||||
|
=> "Conçu pour celles et ceux qui en portent le poids.",
|
||||||
|
"Police, special forces, military, and emergency services share a hard truth: their craft is rehearsed in the worst conditions, and rarely enough. PROSERVE\u{2122} puts contemporary training within everyone's reach \u{2014} every shift, every budget, every operating context."
|
||||||
|
=> "Police, forces spéciales, armée et services d'urgence partagent une vérité dure : leur métier se répète dans les pires conditions, et trop rarement. PROSERVE\u{2122} met l'entraînement contemporain à la portée de tous \u{2014} chaque service, chaque budget, chaque contexte opérationnel.",
|
||||||
|
"Four operating contexts. One platform."
|
||||||
|
=> "Quatre contextes opérationnels. Une seule plateforme.",
|
||||||
|
|
||||||
|
// Card eyebrows + tile labels
|
||||||
|
'National & Municipal' => 'Nationale & Municipale',
|
||||||
|
'Police' => 'Police',
|
||||||
|
"Police training, when the schedule won't bend."
|
||||||
|
=> "L'entraînement police, quand l'agenda ne fléchit pas.",
|
||||||
|
'Tactical units' => 'Unités tactiques',
|
||||||
|
'Special Forces' => 'Forces spéciales',
|
||||||
|
"Special forces train differently. Their simulation should, too."
|
||||||
|
=> "Les forces spéciales s'entraînent autrement. Leur simulation devrait suivre.",
|
||||||
|
'Armed forces' => 'Forces armées',
|
||||||
|
'Military' => 'Armée',
|
||||||
|
"Military VR training. Sovereign. Modular. Field-deployable."
|
||||||
|
=> "Entraînement VR pour l'armée. Souverain. Modulaire. Déployable sur le terrain.",
|
||||||
|
'Emergency services' => "Services d'urgence",
|
||||||
|
'Firefighters' => 'Sapeurs-pompiers',
|
||||||
|
"Train the worst day. Safely. Repeatedly."
|
||||||
|
=> "Entraînez-vous au pire jour. En sécurité. Indéfiniment.",
|
||||||
|
|
||||||
|
// Card body concatenations from hub builder
|
||||||
|
"National & Municipal \u{2014} Police training, when the schedule won't bend."
|
||||||
|
=> "Nationale & Municipale \u{2014} L'entraînement police, quand l'agenda ne fléchit pas.",
|
||||||
|
"Tactical units \u{2014} Special forces train differently. Their simulation should, too."
|
||||||
|
=> "Unités tactiques \u{2014} Les forces spéciales s'entraînent autrement. Leur simulation devrait suivre.",
|
||||||
|
"Armed forces \u{2014} Military VR training. Sovereign. Modular. Field-deployable."
|
||||||
|
=> "Forces armées \u{2014} Entraînement VR pour l'armée. Souverain. Modulaire. Déployable sur le terrain.",
|
||||||
|
"Emergency services \u{2014} Train the worst day. Safely. Repeatedly."
|
||||||
|
=> "Services d'urgence \u{2014} Entraînez-vous au pire jour. En sécurité. Indéfiniment.",
|
||||||
|
|
||||||
|
// Hero / detail-page CTA labels specific to Industries
|
||||||
|
'Request a Demo for police use' => 'Demander une démo pour la police',
|
||||||
|
'Request a Demo for fire & emergency' => "Demander une démo pour pompiers & urgences",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger — fires on template_redirect (NOT init) so Bricks's
|
||||||
|
element controls are loaded (happens on 'wp' hook). Without loaded
|
||||||
|
controls, Bricks's wpml_page_builder_register_strings handler skips
|
||||||
|
every element silently and NO strings reach icl_strings → ATE empty.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_industries'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$data_file = 'C:\\ASTERION\\GIT\\WP2026\\content-source\\industries-data.php';
|
||||||
|
if (! file_exists($data_file)) wp_die("data file not found: $data_file");
|
||||||
|
|
||||||
|
$industries = include $data_file;
|
||||||
|
if (! is_array($industries)) wp_die('industries data not array');
|
||||||
|
|
||||||
|
$parent_id = av_ensure_hub_page('industries', 'Industries');
|
||||||
|
$report = ["Rebuilding /industries/ hub + child detail pages..."];
|
||||||
|
|
||||||
|
// Hub
|
||||||
|
$hub_elements = av_build_industries_hub_elements($industries);
|
||||||
|
$hub_write = av_write_bricks_page_elements($parent_id, $hub_elements);
|
||||||
|
$report[] = " HUB → page #$parent_id (rebuilt) — " . count($hub_elements)
|
||||||
|
. " elements (json={$hub_write['json_len']}, stored={$hub_write['stored_len']})";
|
||||||
|
|
||||||
|
// Detail pages
|
||||||
|
foreach ($industries as $slug => $d) {
|
||||||
|
$page = av_upsert_detail_page($slug, 'industries', $d);
|
||||||
|
$report[] = " $slug → page #{$page['id']} ({$page['action']}) — {$page['element_count']} elements (json={$page['json_len']}, stored={$page['stored_len']})";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Industries hub builder : hero + 4-card grid + closer CTA
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_build_industries_hub_elements($industries) {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// HERO
|
||||||
|
$els = array_merge($els, av_build_hero('hub', [
|
||||||
|
'eyebrow' => 'Industries',
|
||||||
|
'tagline' => "Built for those who carry the weight.",
|
||||||
|
'hero_lead' => "Police, special forces, military, and emergency services share a hard truth: their craft is rehearsed in the worst conditions, and rarely enough. PROSERVE\u{2122} puts contemporary training within everyone's reach \u{2014} every shift, every budget, every operating context.",
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'Book a demo', 'href' => '/request-demo/', 'variant' => 'primary'],
|
||||||
|
['label' => 'Request a T&E Kit', 'href' => '/request-te-kit/', 'variant' => 'secondary'],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// 4-card grid — one card per industry
|
||||||
|
$cards_section = [
|
||||||
|
'eyebrow' => '',
|
||||||
|
'title' => 'Four operating contexts. One platform.',
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
foreach ($industries as $slug => $d) {
|
||||||
|
$cards_section['items'][] = [
|
||||||
|
'title' => $d['title'],
|
||||||
|
'body' => ($d['eyebrow'] ?? '') . " \u{2014} " . ($d['tagline'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$els = array_merge($els, av_build_cards_section('hubgrid', 0, $cards_section, 'var(--color-bg-subtle)'));
|
||||||
|
|
||||||
|
// Append "Learn more →" link in each card (post-build patch)
|
||||||
|
$i = 0;
|
||||||
|
foreach ($industries as $slug => $d) {
|
||||||
|
$kid = "khubgridc0x{$i}";
|
||||||
|
$lid = "lnhubgridc0x{$i}";
|
||||||
|
$els[] = av_text_link($lid, $kid, "Learn more \u{2192}", "/industries/$slug/");
|
||||||
|
foreach ($els as &$el) {
|
||||||
|
if (($el['id'] ?? '') === $kid) {
|
||||||
|
$el['children'][] = $lid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($el);
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLOSER CTA
|
||||||
|
$els = array_merge($els, av_build_closer_cta('hub'));
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
470
tools/av-generate-insights.php
Normal file
470
tools/av-generate-insights.php
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate Insights blog setup (one-shot)
|
||||||
|
* Description: /?av_generate_insights=1 creates :
|
||||||
|
* 1. The /insights/ hub page (hero + Bricks posts query-loop + closer CTA)
|
||||||
|
* 2. A Bricks Single Post template (applied to all posts site-wide)
|
||||||
|
* 3. Three seed posts (sample content) so the hub isn't empty
|
||||||
|
*
|
||||||
|
* Idempotent : re-runs update without duplicating.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
FR strings dict for Insights
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, [
|
||||||
|
'Insights' => 'Analyses',
|
||||||
|
'Field notes from the platform.' => 'Notes de terrain depuis la plateforme.',
|
||||||
|
'Operational realities, tactical considerations, and product updates from the Asterion team. We publish when we have something worth saying.'
|
||||||
|
=> "Réalités opérationnelles, considérations tactiques et mises à jour produit de l'équipe Asterion. Nous publions quand nous avons quelque chose à dire.",
|
||||||
|
'Read article' => "Lire l'article",
|
||||||
|
'Read more' => 'Lire la suite',
|
||||||
|
'Latest articles' => 'Derniers articles',
|
||||||
|
'No articles yet — check back soon.' => "Pas encore d'article — revenez bientôt.",
|
||||||
|
'Subscribe to updates' => 'S\'abonner aux mises à jour',
|
||||||
|
|
||||||
|
// Seed posts FR titles + excerpts
|
||||||
|
'The 10-to-15 gap : why 90 cartridges per year became a fiction'
|
||||||
|
=> "L'écart 10 à 15 : pourquoi 90 cartouches par an est devenu une fiction",
|
||||||
|
'French regulation sets a clear floor for police marksmanship training. Reality, almost everywhere, falls short. Here is what the gap looks like, why it persists, and what closing it operationally actually means.'
|
||||||
|
=> "La réglementation française fixe un plancher clair pour le tir police. La réalité, presque partout, est en deçà. Voici à quoi ressemble cet écart, pourquoi il persiste et ce que le combler signifie en pratique.",
|
||||||
|
|
||||||
|
'Why simulators built for entertainment break in tactical training'
|
||||||
|
=> "Pourquoi les simulateurs conçus pour le divertissement cassent en entraînement tactique",
|
||||||
|
'Most VR shooting simulators were engineered with one priority : visual realism. For tactical training, that is the wrong priority. Here is what we learned building the platform the other way around.'
|
||||||
|
=> "La plupart des simulateurs VR ont été conçus avec une priorité : le réalisme visuel. Pour l'entraînement tactique, c'est la mauvaise priorité. Voici ce qu'on a appris en construisant la plateforme dans l'autre sens.",
|
||||||
|
|
||||||
|
'Sovereign by default : what GDPR-native actually means for security forces'
|
||||||
|
=> "Souverain par défaut : ce que RGPD-natif veut vraiment dire pour les forces de sécurité",
|
||||||
|
'There is a difference between a product that complies with GDPR and one designed for it from the start. For training data \u{2014} sensitive by definition \u{2014} the difference matters.'
|
||||||
|
=> "Il y a une différence entre un produit conforme RGPD et un produit conçu pour l'être dès l'origine. Pour les données d'entraînement \u{2014} sensibles par définition \u{2014} cette différence compte.",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_insights'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
/* 1. Hub page /insights/ */
|
||||||
|
$hub_id = av_ensure_hub_page('insights', 'Insights');
|
||||||
|
$elements = av_build_insights_hub_elements();
|
||||||
|
$write = av_write_bricks_page_elements($hub_id, $elements);
|
||||||
|
echo "Hub /insights/ → page #$hub_id — " . count($elements) . " elements (json={$write['json_len']}, stored={$write['stored_len']})\n\n";
|
||||||
|
|
||||||
|
/* 2. Bricks Single Post template */
|
||||||
|
$tpl_id = av_ensure_insight_single_template();
|
||||||
|
echo "Single Post template → bricks_template #$tpl_id\n\n";
|
||||||
|
|
||||||
|
/* 3. Seed posts */
|
||||||
|
echo "Seed posts :\n";
|
||||||
|
$posts = av_get_insight_seed_posts();
|
||||||
|
foreach ($posts as $p) {
|
||||||
|
$existing = get_page_by_path($p['slug'], OBJECT, 'post');
|
||||||
|
if ($existing) {
|
||||||
|
wp_update_post([
|
||||||
|
'ID' => $existing->ID,
|
||||||
|
'post_title' => $p['title'],
|
||||||
|
'post_excerpt' => $p['excerpt'],
|
||||||
|
'post_content' => $p['content'],
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_date' => $p['date'],
|
||||||
|
]);
|
||||||
|
echo " '{$p['title']}' → post #{$existing->ID} (updated)\n";
|
||||||
|
} else {
|
||||||
|
$new = wp_insert_post([
|
||||||
|
'post_type' => 'post',
|
||||||
|
'post_title' => $p['title'],
|
||||||
|
'post_name' => $p['slug'],
|
||||||
|
'post_excerpt' => $p['excerpt'],
|
||||||
|
'post_content' => $p['content'],
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_date' => $p['date'],
|
||||||
|
]);
|
||||||
|
echo " '{$p['title']}' → post #$new (created, slug={$p['slug']})\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Visit /insights/ to see the blog hub. Click any post for the single template.\n";
|
||||||
|
echo "Write more posts via WP Admin → Posts → Add New.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
HUB PAGE BUILDER : hero + posts query-loop + closer
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_build_insights_hub_elements() {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// HERO
|
||||||
|
$els = array_merge($els, av_build_hero('ins', [
|
||||||
|
'eyebrow' => 'Insights',
|
||||||
|
'tagline' => 'Field notes from the platform.',
|
||||||
|
'hero_lead' => 'Operational realities, tactical considerations, and product updates from the Asterion team. We publish when we have something worth saying.',
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'Subscribe to updates', 'href' => '/contact/', 'variant' => 'primary'],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// POSTS QUERY-LOOP SECTION
|
||||||
|
$sid = 'sinslst';
|
||||||
|
$oct = 'oinslst';
|
||||||
|
$nrr = 'ninslst';
|
||||||
|
$eyb = 'einslst';
|
||||||
|
$hId = 'hinslst';
|
||||||
|
$pst = 'pinslst'; // posts element
|
||||||
|
|
||||||
|
$els[] = [
|
||||||
|
'id' => $sid, 'name' => 'section', 'parent' => 0, 'children' => [$oct],
|
||||||
|
'settings' => array_merge(av_section_padding_responsive(), [
|
||||||
|
'_background' => ['color' => av_color('var(--color-bg-default)')],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $oct, 'name' => 'container', 'parent' => $sid, 'children' => [$nrr, $pst],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '1280px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_padding' => av_sp('0', '1.5rem', '0', '1.5rem'),
|
||||||
|
'_padding:mobile_landscape' => av_sp('0', '1rem', '0', '1rem'),
|
||||||
|
'_alignItems' => 'flex-start',
|
||||||
|
'_rowGap' => '2rem',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $nrr, 'name' => 'container', 'parent' => $oct, 'children' => [$eyb, $hId],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '800px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_margin' => av_sp('0', 'auto', '0', '0'),
|
||||||
|
'_padding' => av_sp('0', '0', '0', '0'),
|
||||||
|
'_rowGap' => '0.75rem',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = av_eyebrow($eyb, $nrr, 'Latest articles');
|
||||||
|
$els[] = av_heading($hId, $nrr, 'h2', 'Field notes from the platform.');
|
||||||
|
|
||||||
|
// The Bricks `posts` element. Each card's content is defined by the
|
||||||
|
// `fields` repeater (dynamic data + tag + styling per field).
|
||||||
|
// Without these fields, Bricks renders empty <li> wrappers only.
|
||||||
|
$els[] = [
|
||||||
|
'id' => $pst, 'name' => 'posts', 'parent' => $oct, 'children' => [],
|
||||||
|
'settings' => [
|
||||||
|
'query' => [
|
||||||
|
'objectType' => 'post',
|
||||||
|
'post_type' => 'post',
|
||||||
|
'posts_per_page' => 9,
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'DESC',
|
||||||
|
],
|
||||||
|
'columns' => 3,
|
||||||
|
'gutter' => 24,
|
||||||
|
'layout' => 'grid',
|
||||||
|
'linkPost' => true, // make the whole card clickable
|
||||||
|
'imageDisable' => true, // no featured image for now
|
||||||
|
// Card structure
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'id' => substr(md5('insf-eyebrow'), 0, 6),
|
||||||
|
'dynamicData' => 'Insights',
|
||||||
|
'tag' => 'div',
|
||||||
|
'dynamicTypography' => [
|
||||||
|
'color' => av_color('var(--color-brand-gold)'),
|
||||||
|
'font-size' => '0.75rem',
|
||||||
|
'font-weight' => '600',
|
||||||
|
'text-transform' => 'uppercase',
|
||||||
|
'letter-spacing' => '0.08em',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => substr(md5('insf-title'), 0, 6),
|
||||||
|
'dynamicData' => '{post_title}',
|
||||||
|
'tag' => 'h3',
|
||||||
|
'dynamicTypography' => [
|
||||||
|
'color' => av_color('var(--color-text-primary)'),
|
||||||
|
'font-size' => '1.125rem',
|
||||||
|
'font-weight' => '700',
|
||||||
|
'line-height' => '1.35',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => substr(md5('insf-meta'), 0, 6),
|
||||||
|
'dynamicData' => '{post_date}',
|
||||||
|
'tag' => 'div',
|
||||||
|
'dynamicTypography' => [
|
||||||
|
'color' => av_color('var(--color-text-tertiary)'),
|
||||||
|
'font-size' => '0.8125rem',
|
||||||
|
'line-height' => '1.4',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => substr(md5('insf-excerpt'), 0, 6),
|
||||||
|
'dynamicData' => '{post_excerpt}',
|
||||||
|
'tag' => 'p',
|
||||||
|
'dynamicTypography' => [
|
||||||
|
'color' => av_color('var(--color-text-secondary)'),
|
||||||
|
'font-size' => '0.9375rem',
|
||||||
|
'line-height' => '1.55',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// Card wrapper styling via custom CSS (Bricks's repeater wrapper :
|
||||||
|
// .bricks-layout-inner is the card body)
|
||||||
|
'_cssCustom' => "%root% .bricks-layout-inner { padding: 1.5rem; background: var(--color-bg-subtle); border: 1px solid var(--color-border-subtle); border-radius: 4px; display: flex; flex-direction: column; gap: 0.6rem; height: 100%; }\n%root% .bricks-layout-item { display: flex; }\n%root% .bricks-layout-item a { text-decoration: none; }",
|
||||||
|
'_width' => '100%',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// CLOSER CTA
|
||||||
|
$els = array_merge($els, av_build_closer_cta('ins'));
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
BRICKS SINGLE POST TEMPLATE : hero (title + meta) + content + closer
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_ensure_insight_single_template() {
|
||||||
|
// Find existing or create
|
||||||
|
$existing = get_posts([
|
||||||
|
'post_type' => 'bricks_template',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'name' => 'single-post-asterion',
|
||||||
|
'numberposts' => 1,
|
||||||
|
]);
|
||||||
|
if ($existing) {
|
||||||
|
$tpl_id = $existing[0]->ID;
|
||||||
|
wp_update_post(['ID' => $tpl_id, 'post_title' => 'Single Post — Asterion']);
|
||||||
|
} else {
|
||||||
|
$tpl_id = wp_insert_post([
|
||||||
|
'post_type' => 'bricks_template',
|
||||||
|
'post_title' => 'Single Post — Asterion',
|
||||||
|
'post_name' => 'single-post-asterion',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as type=single + apply to all single posts
|
||||||
|
update_post_meta($tpl_id, '_bricks_template_type', 'single');
|
||||||
|
$settings = get_post_meta($tpl_id, '_bricks_template_settings', true) ?: [];
|
||||||
|
$settings['templateConditions'] = [
|
||||||
|
['id' => substr(md5('postSingle'), 0, 6), 'main' => 'singular', 'object' => 'post'],
|
||||||
|
];
|
||||||
|
update_post_meta($tpl_id, '_bricks_template_settings', $settings);
|
||||||
|
|
||||||
|
// Build template content
|
||||||
|
$elements = av_build_insight_single_elements();
|
||||||
|
av_write_bricks_page_elements($tpl_id, $elements);
|
||||||
|
|
||||||
|
return $tpl_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_build_insight_single_elements() {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// 1. Hero — eyebrow + post title + meta + featured image
|
||||||
|
$sid = 'spostsec';
|
||||||
|
$oct = 'spostoc';
|
||||||
|
$nrr = 'spostnr';
|
||||||
|
$eyb = 'spostey';
|
||||||
|
$hId = 'sposth1';
|
||||||
|
$met = 'spostmt';
|
||||||
|
$img = 'spostim';
|
||||||
|
|
||||||
|
$els[] = [
|
||||||
|
'id' => $sid, 'name' => 'section', 'parent' => 0, 'children' => [$oct],
|
||||||
|
'settings' => [
|
||||||
|
'_padding' => av_sp('5rem', '0', '4rem', '0'),
|
||||||
|
'_padding:tablet_portrait' => av_sp('4rem', '0', '3rem', '0'),
|
||||||
|
'_padding:mobile_landscape' => av_sp('3rem', '0', '2.5rem', '0'),
|
||||||
|
'_padding:mobile_portrait' => av_sp('2.5rem', '0', '2rem', '0'),
|
||||||
|
'_background' => ['color' => av_color('var(--color-bg-default)')],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $oct, 'name' => 'container', 'parent' => $sid, 'children' => [$nrr, $img],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '1280px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_padding' => av_sp('0', '1.5rem', '0', '1.5rem'),
|
||||||
|
'_padding:mobile_landscape' => av_sp('0', '1rem', '0', '1rem'),
|
||||||
|
'_alignItems' => 'flex-start',
|
||||||
|
'_rowGap' => '2rem',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $nrr, 'name' => 'container', 'parent' => $oct, 'children' => [$eyb, $hId, $met],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '800px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_margin' => av_sp('0', 'auto', '0', '0'),
|
||||||
|
'_padding' => av_sp('0', '0', '0', '0'),
|
||||||
|
'_rowGap' => '1rem',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = av_eyebrow($eyb, $nrr, 'Insights');
|
||||||
|
// Heading uses dynamic data : {post_title}
|
||||||
|
$els[] = [
|
||||||
|
'id' => $hId, 'name' => 'heading', 'parent' => $nrr, 'children' => [],
|
||||||
|
'settings' => [
|
||||||
|
'tag' => 'h1',
|
||||||
|
'text' => '{post_title}',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
// Meta : {post_date} · {post_author}
|
||||||
|
$els[] = av_text($met, $nrr, '{post_date} \u{B7} {post_author}', 'var(--color-text-tertiary)', '0.875rem', '1.5');
|
||||||
|
|
||||||
|
// Featured image (Bricks image element with dynamic data)
|
||||||
|
$els[] = [
|
||||||
|
'id' => $img, 'name' => 'image', 'parent' => $oct, 'children' => [],
|
||||||
|
'settings' => [
|
||||||
|
'image' => ['useDynamicData' => '{featured_image}'],
|
||||||
|
'_widthMax' => '1280px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_aspectRatio' => '16/9',
|
||||||
|
'_objectFit' => 'cover',
|
||||||
|
'_border' => ['radius' => av_sp('4px', '4px', '4px', '4px')],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 2. Content section
|
||||||
|
$csec = 'spostcc';
|
||||||
|
$coct = 'spostco';
|
||||||
|
$cnrr = 'spostcn';
|
||||||
|
$cpc = 'spostpc'; // post-content element
|
||||||
|
|
||||||
|
$els[] = [
|
||||||
|
'id' => $csec, 'name' => 'section', 'parent' => 0, 'children' => [$coct],
|
||||||
|
'settings' => array_merge(av_section_padding_responsive('3rem', '5rem'), [
|
||||||
|
'_background' => ['color' => av_color('var(--color-bg-default)')],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $coct, 'name' => 'container', 'parent' => $csec, 'children' => [$cnrr],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '1280px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_padding' => av_sp('0', '1.5rem', '0', '1.5rem'),
|
||||||
|
'_padding:mobile_landscape' => av_sp('0', '1rem', '0', '1rem'),
|
||||||
|
'_alignItems' => 'flex-start',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $cnrr, 'name' => 'container', 'parent' => $coct, 'children' => [$cpc],
|
||||||
|
'settings' => [
|
||||||
|
'_direction' => 'column',
|
||||||
|
'_widthMax' => '800px',
|
||||||
|
'_width' => '100%',
|
||||||
|
'_margin' => av_sp('0', 'auto', '0', '0'),
|
||||||
|
'_padding' => av_sp('0', '0', '0', '0'),
|
||||||
|
'_rowGap' => '1rem',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$els[] = [
|
||||||
|
'id' => $cpc, 'name' => 'post-content', 'parent' => $cnrr, 'children' => [],
|
||||||
|
'settings' => [
|
||||||
|
// Rendered by Bricks's post-content element : the post body
|
||||||
|
'_typography' => [
|
||||||
|
'color' => av_color('var(--color-text-secondary)'),
|
||||||
|
'font-size' => '1.0625rem',
|
||||||
|
'line-height' => '1.65',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3. Closer CTA (re-uses shared)
|
||||||
|
$els = array_merge($els, av_build_closer_cta('spost'));
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
SEED POSTS : 3 sample articles to populate the blog hub
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_get_insight_seed_posts() {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'slug' => 'the-10-to-15-gap',
|
||||||
|
'title' => 'The 10-to-15 gap : why 90 cartridges per year became a fiction',
|
||||||
|
'date' => '2026-04-22 10:00:00',
|
||||||
|
'excerpt' => 'French regulation sets a clear floor for police marksmanship training. Reality, almost everywhere, falls short. Here is what the gap looks like, why it persists, and what closing it operationally actually means.',
|
||||||
|
'content' => "<p>The number is officially 90. Ninety cartridges per officer per year — the regulatory minimum for French police marksmanship training. Officers know the number. Trainers know the number. The administrative records track the number. And the median actual figure, when you measure it across regional units, sits at <strong>10 to 15 cartridges per officer per year</strong>.</p>
|
||||||
|
|
||||||
|
<p>This is not a story of negligence. The infrastructure is saturated. Live ranges are booked weeks ahead. Instructor time is finite. Ammunition budgets are constrained. The gap exists because the system cannot scale the way training needs scale.</p>
|
||||||
|
|
||||||
|
<h2>Why the gap matters operationally</h2>
|
||||||
|
|
||||||
|
<p>Marksmanship is not the goal. The goal is decision-making under stress. Marksmanship is the substrate that makes everything else possible. When repetition collapses by an order of magnitude, the substrate erodes. We see it in the data : tactical errors under stress rise. Time-to-first-shot under contact extends. Target identification accuracy degrades.</p>
|
||||||
|
|
||||||
|
<p>The fix cannot come from building more ranges. It has to come from changing what counts as a training repetition.</p>
|
||||||
|
|
||||||
|
<h2>What VR changes</h2>
|
||||||
|
|
||||||
|
<p>VR training does not replace live fire. It changes the economics of the next repetition. A live-fire drill costs ammunition, range time, instructor time, decontamination. A VR drill costs none of those, and produces measurable data the live one cannot.</p>
|
||||||
|
|
||||||
|
<p>If you can do twenty VR drills for the cost of one live one, the median annual repetition figure becomes 200 to 300 — not 10 to 15. And the question of whether the regulatory floor of 90 is even ambitious enough starts to look different.</p>",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'slug' => 'simulators-for-entertainment-vs-training',
|
||||||
|
'title' => 'Why simulators built for entertainment break in tactical training',
|
||||||
|
'date' => '2026-03-18 14:30:00',
|
||||||
|
'excerpt' => 'Most VR shooting simulators were engineered with one priority : visual realism. For tactical training, that is the wrong priority. Here is what we learned building the platform the other way around.',
|
||||||
|
'content' => "<p>If you have spent any time evaluating VR training products, you will have noticed a pattern. The marketing leads with the visuals. The screenshots are cinematic. The demos open with photoreal cityscapes and dramatic lighting.</p>
|
||||||
|
|
||||||
|
<p>This is what entertainment-first engineering looks like. And it is the wrong way to build a training tool.</p>
|
||||||
|
|
||||||
|
<h2>Three reasons it breaks</h2>
|
||||||
|
|
||||||
|
<p><strong>1. Ballistics are an afterthought.</strong> If the engine was built to render a beautiful frame at thirty meters, it was probably not built to compute trajectory, drag, and material penetration accurately at three meters. We have seen products where bullets stopped at the texture surface, where ricochets ignored angle of impact, where through-cover engagements were not modeled at all.</p>
|
||||||
|
|
||||||
|
<p><strong>2. AI is scripted.</strong> When civilians are on rails — walk this path, react this way to this trigger — operators learn the script, not the discipline. The drill becomes pattern recognition. Real engagements do not have patterns.</p>
|
||||||
|
|
||||||
|
<p><strong>3. The instructor sees what the trainee sees.</strong> A training tool needs to give the supervisor a vantage point the operator does not have. Walk through walls. Switch into any trainee's view. Pause without breaking immersion. Most entertainment-derived products show the instructor a duplicate of one trainee's HUD.</p>
|
||||||
|
|
||||||
|
<h2>What it costs to invert the priorities</h2>
|
||||||
|
|
||||||
|
<p>Building physics-first, AI-behavior-second, photoreal-third is harder, slower, and visibly less impressive in a demo. We accept that. The trade-off is a training tool that actually trains.</p>",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'slug' => 'sovereign-by-default-gdpr-native',
|
||||||
|
'title' => 'Sovereign by default : what GDPR-native actually means for security forces',
|
||||||
|
'date' => '2026-02-05 09:15:00',
|
||||||
|
'excerpt' => "There is a difference between a product that complies with GDPR and one designed for it from the start. For training data \u{2014} sensitive by definition \u{2014} the difference matters.",
|
||||||
|
'content' => "<p>GDPR compliance is a checkbox on many vendor pitch decks. It usually means : we have a privacy policy, we let users export their data, we encrypt connections to our cloud.</p>
|
||||||
|
|
||||||
|
<p>For training data generated by security forces, this is not enough.</p>
|
||||||
|
|
||||||
|
<h2>What 'native' means</h2>
|
||||||
|
|
||||||
|
<p>A platform is GDPR-native when the architecture decisions were made under privacy constraints from day one. Not bolted on later. Concretely :</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Data stays local.</strong> No telemetry leaves the customer's network. No analytics fired at our servers. No remote logging.</li>
|
||||||
|
<li><strong>Identity stays local.</strong> Authentication runs against the customer's directory (Active Directory, LDAP), not a vendor cloud account.</li>
|
||||||
|
<li><strong>Updates are offline-capable.</strong> Air-gapped facilities can run the platform indefinitely without ever needing to phone home.</li>
|
||||||
|
<li><strong>No US Cloud Act exposure.</strong> We do not use US-controlled cloud services for storage or compute. European or sovereign-allied only.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Why this matters for tactical units</h2>
|
||||||
|
|
||||||
|
<p>Training data for a tactical unit includes : the geometry of their training facility, their operational doctrine, their reaction times, their identification accuracy, their squad composition. This is intelligence-grade material.</p>
|
||||||
|
|
||||||
|
<p>A vendor cloud that hosts this data, however well-encrypted, is a single point of compromise — and a single point of jurisdictional exposure. The only safe handling is to make sure the data never has to leave.</p>
|
||||||
|
|
||||||
|
<p>That is not a feature we added. It is the constraint we designed under.</p>",
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
378
tools/av-generate-solutions.php
Normal file
378
tools/av-generate-solutions.php
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate solution pages (one-shot)
|
||||||
|
* Description: /?av_generate_solutions=1 reads content-source/solutions-data.php
|
||||||
|
* and creates/updates : hub /solutions/ + 4 child pages (my-proserve,
|
||||||
|
* proserve-flex, proserve-academy, customization). Idempotent.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Register Solutions-specific FR strings into the shared dict.
|
||||||
|
Brand names (MY PROSERVE, PROSERVE FLEX, PROSERVE ACADEMY, PROSERVE+,
|
||||||
|
PROSERVE™, ASTERION VR™) are kept verbatim.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, av_solutions_fr_dict());
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_solutions_fr_dict() {
|
||||||
|
return [
|
||||||
|
/* ============================ HUB ============================ */
|
||||||
|
'Solutions' => 'Solutions',
|
||||||
|
'PROSERVE platform' => 'Plateforme PROSERVE',
|
||||||
|
"Four tiers. One platform. Train where you are." => "Quatre paliers. Une plateforme. Entraînez-vous là où vous êtes.",
|
||||||
|
"From the solo officer in a 9 m\u{B2} corner to a 16-trainee free-roam academy : PROSERVE\u{2122} scales without changing software. Same physics. Same AI. Same instructor logic. Pick the configuration that fits your context — upgrade or extend later."
|
||||||
|
=> "De l'agent seul dans 9 m\u{B2} à une académie en libre déambulation pour 16 stagiaires : PROSERVE\u{2122} évolue sans changer de logiciel. Mêmes physiques. Même IA. Même logique instructeur. Choisissez la configuration adaptée à votre contexte — étendez ou faites évoluer plus tard.",
|
||||||
|
"Pick the right configuration for your unit." => "Choisissez la configuration adaptée à votre unité.",
|
||||||
|
|
||||||
|
/* -------- Card titles + bodies (hub grid) -------- */
|
||||||
|
'MY PROSERVE' => 'MY PROSERVE',
|
||||||
|
'PROSERVE FLEX' => 'PROSERVE FLEX',
|
||||||
|
'PROSERVE ACADEMY' => 'PROSERVE ACADEMY',
|
||||||
|
'PROSERVE+' => 'PROSERVE+',
|
||||||
|
|
||||||
|
'Solo training' => 'Entraînement solo',
|
||||||
|
'Team training · 1 to 5' => 'Entraînement en équipe · 1 à 5',
|
||||||
|
'Unit training · 1 to 16' => 'Entraînement d\'unité · 1 à 16',
|
||||||
|
'Customization & options' => 'Personnalisation & options',
|
||||||
|
|
||||||
|
'Your first range, in a mini-case.'
|
||||||
|
=> 'Votre premier stand, dans un mini-coffre.',
|
||||||
|
'Your team, in the same scenario, on any flat surface.'
|
||||||
|
=> 'Votre équipe, dans le même scénario, sur n\'importe quelle surface plane.',
|
||||||
|
'The full training centre, in your facility.'
|
||||||
|
=> 'Le centre de formation complet, dans vos locaux.',
|
||||||
|
"When the catalog doesn't fit your mission."
|
||||||
|
=> "Quand le catalogue ne correspond pas à votre mission.",
|
||||||
|
|
||||||
|
// Card body concatenations from hub builder (eyebrow — tagline)
|
||||||
|
"Solo training \u{2014} Your first range, in a mini-case."
|
||||||
|
=> "Entraînement solo \u{2014} Votre premier stand, dans un mini-coffre.",
|
||||||
|
"Team training \u{B7} 1 to 5 \u{2014} Your team, in the same scenario, on any flat surface."
|
||||||
|
=> "Entraînement en équipe \u{B7} 1 à 5 \u{2014} Votre équipe, dans le même scénario, sur n'importe quelle surface plane.",
|
||||||
|
"Unit training \u{B7} 1 to 16 \u{2014} The full training centre, in your facility."
|
||||||
|
=> "Entraînement d'unité \u{B7} 1 à 16 \u{2014} Le centre de formation complet, dans vos locaux.",
|
||||||
|
"Customization & options \u{2014} When the catalog doesn't fit your mission."
|
||||||
|
=> "Personnalisation & options \u{2014} Quand le catalogue ne correspond pas à votre mission.",
|
||||||
|
|
||||||
|
/* ====================== MY PROSERVE ====================== */
|
||||||
|
"French regulation requires three live-fire sessions and 90 cartridges per officer per year. Reality: 10 to 15. MY PROSERVE closes that gap, every shift, in 9 square meters."
|
||||||
|
=> "La réglementation française exige trois séances de tir réel et 90 cartouches par agent et par an. La réalité : 10 à 15. MY PROSERVE comble cet écart, à chaque service, en 9 mètres carrés.",
|
||||||
|
|
||||||
|
'Why solo training matters now' => "Pourquoi l'entraînement solo compte maintenant",
|
||||||
|
'The training deficit is operational risk.'
|
||||||
|
=> "Le déficit d'entraînement est un risque opérationnel.",
|
||||||
|
"Two facts shape this product. First, regulation sets a floor that almost no force consistently meets. Second, every officer issued a firearm — including municipal police, gendarmerie reservists, and protective security agents — needs more repetition than range schedules can deliver. MY PROSERVE answers both: it puts a personal range in 3 to 9 m\u{B2} of free floor space, available 24/7, with the same physics as the live one."
|
||||||
|
=> "Deux faits structurent ce produit. D'abord, la réglementation fixe un plancher que presque aucune force ne respecte vraiment. Ensuite, chaque agent porteur d'arme — y compris les polices municipales, les réservistes de la gendarmerie et les agents de sécurité — a besoin de plus de répétition que le planning des stands ne peut offrir. MY PROSERVE répond aux deux : un stand personnel dans 3 à 9 m\u{B2} de surface libre, disponible 24h/24, avec la même physique que le tir réel.",
|
||||||
|
|
||||||
|
'Key benefits' => 'Bénéfices clés',
|
||||||
|
'What you get with one mini-case.' => 'Ce que vous obtenez avec un seul mini-coffre.',
|
||||||
|
|
||||||
|
'Ultra-portable' => 'Ultra-portable',
|
||||||
|
'Fits in a single mini-case sized for routine transport. Weighs less than 20 kg. Sets up in under five minutes.'
|
||||||
|
=> "Tient dans un seul mini-coffre dimensionné pour le transport quotidien. Pèse moins de 20 kg. Installation en moins de cinq minutes.",
|
||||||
|
'Available around the clock' => 'Disponible 24h/24',
|
||||||
|
'No range scheduling, no firearms instructor required for routine drills. Train at the start of a shift, between two patrols, or before deployment.'
|
||||||
|
=> "Pas de réservation de stand, pas d'instructeur de tir requis pour les exercices courants. Entraînez-vous au début d'un service, entre deux patrouilles ou avant une mission.",
|
||||||
|
'Multi-site' => 'Multi-sites',
|
||||||
|
'Move it from station to station. Equip a region with a small fleet — your officers train where they are stationed.'
|
||||||
|
=> "Déplacez-le d'un poste à l'autre. Équipez une région avec une petite flotte — vos agents s'entraînent là où ils sont affectés.",
|
||||||
|
'Economical' => 'Économique',
|
||||||
|
"Ammunition costs collapse. A CO2 refill costs about \u{20AC}0.25 and powers 30 to 40 shots. A gas refill costs about \u{20AC}8 and powers 600 to 800 shots. Break-even versus live ammunition arrives in months, not years."
|
||||||
|
=> "Les coûts de munitions s'effondrent. Une recharge CO2 coûte environ 0,25 \u{20AC} et permet 30 à 40 tirs. Une recharge gaz coûte environ 8 \u{20AC} et permet 600 à 800 tirs. Le retour sur investissement face aux munitions réelles se compte en mois, pas en années.",
|
||||||
|
'Real-recoil firearm' => 'Arme à recul réel',
|
||||||
|
'A gas-blowback or CO2 replica fitted with our Universal Weapon Digitizer. Recoil is genuine. Shot detection is dual (sound + recoil) to avoid false positives.'
|
||||||
|
=> "Une réplique gas-blowback ou CO2 équipée de notre Universal Weapon Digitizer. Le recul est authentique. La détection du tir est double (son + recul) pour éviter les faux positifs.",
|
||||||
|
'Personal AAR' => 'AAR personnel',
|
||||||
|
'Every officer has a personal account and dashboard. Track precision, reaction time, missed shots, target identification accuracy over time. Shareable with the supervising instructor.'
|
||||||
|
=> "Chaque agent dispose d'un compte personnel et d'un tableau de bord. Suivez précision, temps de réaction, tirs manqués et identification des cibles dans le temps. Partageable avec l'instructeur référent.",
|
||||||
|
|
||||||
|
"What's in the mini-case" => 'Le contenu du mini-coffre',
|
||||||
|
'Inside the case.' => 'Dans le coffre.',
|
||||||
|
'VR rendering laptop (Intel Core i9-13900HX · NVIDIA RTX 4080 · 16 GB DDR5 · Wi-Fi 6E)'
|
||||||
|
=> 'Portable de rendu VR (Intel Core i9-13900HX · NVIDIA RTX 4080 · 16 Go DDR5 · Wi-Fi 6E)',
|
||||||
|
"5K wireless headset (per-eye 2448\u{D7}2448 \u{B7} 120\u{B0} FOV \u{B7} Snapdragon XR2)"
|
||||||
|
=> "Casque sans fil 5K (2448\u{D7}2448 par \u{153}il \u{B7} 120\u{B0} FOV \u{B7} Snapdragon XR2)",
|
||||||
|
'Spare battery + charging dock' => 'Batterie de rechange + station de charge',
|
||||||
|
'Wi-Fi 6E router (tri-band, AXE11000)' => 'Routeur Wi-Fi 6E (tri-bande, AXE11000)',
|
||||||
|
'One real-recoil replica (handgun OR rifle, your choice from the catalog)'
|
||||||
|
=> 'Une réplique à recul réel (pistolet OU fusil, au choix dans le catalogue)',
|
||||||
|
'Universal Weapon Digitizer (UWeD)' => 'Universal Weapon Digitizer (UWeD)',
|
||||||
|
'Gas / CO2 starter pack' => 'Pack de démarrage gaz / CO2',
|
||||||
|
"Custom-foam waterproof transport case (~20 kg fully loaded)"
|
||||||
|
=> "Coffre de transport étanche avec mousse sur mesure (~20 kg en charge)",
|
||||||
|
|
||||||
|
'Suitable for' => 'Adapté à',
|
||||||
|
'Designed for the officer, not the academy.' => "Conçu pour l'agent, pas l'académie.",
|
||||||
|
'Police municipale and protective security agents — comply with continuous training obligations without depending on a national range network.'
|
||||||
|
=> "Police municipale et agents de sécurité — respectez les obligations de formation continue sans dépendre d'un réseau national de stands.",
|
||||||
|
'Reservists and gendarmes — train at home base between mobilizations.'
|
||||||
|
=> "Réservistes et gendarmes — entraînez-vous à votre base entre deux mobilisations.",
|
||||||
|
'Firearm-licensed officers in transport, energy, or critical infrastructure security.'
|
||||||
|
=> "Agents porteurs d'arme dans la sécurité des transports, de l'énergie ou des infrastructures critiques.",
|
||||||
|
'Trainee officers preparing for live-fire qualifications.'
|
||||||
|
=> 'Élèves préparant leurs qualifications de tir réel.',
|
||||||
|
|
||||||
|
'Volume pricing' => 'Tarif volume',
|
||||||
|
'Volume pricing for regional rollouts.' => 'Tarif volume pour les déploiements régionaux.',
|
||||||
|
"Equip a department, a region, or a national programme. Indicative tiers: from \u{20AC}9,000 per unit at single-piece purchase, dropping to \u{20AC}6,000 per unit at 50+ units. Quote on request."
|
||||||
|
=> "Équipez un département, une région ou un programme national. Tarifs indicatifs : à partir de 9 000 \u{20AC} l'unité à l'achat unitaire, descendant à 6 000 \u{20AC} l'unité au-delà de 50 unités. Devis sur demande.",
|
||||||
|
|
||||||
|
'When to size up.' => "Quand passer à la taille supérieure.",
|
||||||
|
'MY PROSERVE is the right choice for solo, repetitive marksmanship and decision drills. If you need cooperative team training (multiple trainees in the same scenario, real-time tactical coordination), step up to PROSERVE FLEX. If you run a full curriculum for 8 to 16 trainees at once, PROSERVE ACADEMY is the right fit. The software is the same — only the format scales.'
|
||||||
|
=> "MY PROSERVE est le bon choix pour le tir et les drills décisionnels solo et répétés. Si vous avez besoin d'entraînement coopératif (plusieurs stagiaires dans le même scénario, coordination tactique en temps réel), montez à PROSERVE FLEX. Si vous opérez un cursus complet pour 8 à 16 stagiaires en simultané, PROSERVE ACADEMY est adapté. Le logiciel est le même — seul le format évolue.",
|
||||||
|
'Compare with PROSERVE FLEX' => 'Comparer avec PROSERVE FLEX',
|
||||||
|
'Compare with PROSERVE ACADEMY' => 'Comparer avec PROSERVE ACADEMY',
|
||||||
|
|
||||||
|
/* ====================== PROSERVE FLEX ====================== */
|
||||||
|
'The portable team kit. One case per operator, one instructor PC, full multi-user cooperation. Deploy on a station floor, in a school gym, in a tent. The whole unit trains together — and reviews together.'
|
||||||
|
=> "Le kit équipe portable. Un coffre par opérateur, un PC instructeur, coopération multi-utilisateurs complète. Déployez-le sur le sol d'un poste, dans un gymnase, sous une tente. Toute l'unité s'entraîne ensemble — et débriefe ensemble.",
|
||||||
|
|
||||||
|
'Why team training in VR' => "Pourquoi l'entraînement en équipe en VR",
|
||||||
|
'Tactical performance is collective. Train it that way.'
|
||||||
|
=> "La performance tactique est collective. Entraînez-la comme telle.",
|
||||||
|
'Solo drills build muscle memory. They do not build cohesion. PROSERVE FLEX puts up to five operators in the same scenario, seeing each other as 3D avatars in roles defined by the mission — point, breacher, security, K9 handler, sniper. Communication, coordination, command-of-fire decisions: everything an instructor can rate, the system records.'
|
||||||
|
=> "Les exercices solo construisent la mémoire musculaire. Pas la cohésion. PROSERVE FLEX réunit jusqu'à cinq opérateurs dans le même scénario, se voyant comme des avatars 3D dans des rôles définis par la mission — pointe, breacher, sécurité, maître-chien, tireur d'élite. Communication, coordination, décisions de tir : tout ce qu'un instructeur peut noter, le système l'enregistre.",
|
||||||
|
|
||||||
|
'Why FLEX wins on mobility.' => 'Pourquoi FLEX gagne sur la mobilité.',
|
||||||
|
'Versatile' => 'Polyvalent',
|
||||||
|
'Solo, two-by-two, or in teams of up to five. Same hardware, different scenario. The kit adapts to the schedule, not the other way around.'
|
||||||
|
=> "Solo, par binôme ou en équipe jusqu'à cinq. Même matériel, scénarios différents. Le kit s'adapte au planning, pas l'inverse.",
|
||||||
|
'Mobile' => 'Mobile',
|
||||||
|
"One case per operator — fully wireless. No fixed install, no cable management, no specialized facility. Set up on any flat indoor surface from 9 m\u{B2} (solo) to 100 m\u{B2} (full team)."
|
||||||
|
=> "Un coffre par opérateur — entièrement sans fil. Pas d'installation fixe, pas de câbles, pas de local spécifique. Déployez-vous sur toute surface plane intérieure, de 9 m\u{B2} (solo) à 100 m\u{B2} (équipe complète).",
|
||||||
|
'Fast' => 'Rapide',
|
||||||
|
'Solo configuration ready in under five minutes. Full team configuration ready in under twenty. The instructor PC handles calibration on the fly.'
|
||||||
|
=> "Configuration solo prête en moins de cinq minutes. Configuration équipe complète prête en moins de vingt. Le PC instructeur gère la calibration à la volée.",
|
||||||
|
'Immersive' => 'Immersif',
|
||||||
|
'Real-recoil replicas, ballistic physics powered by Unreal Engine 5, autonomous AI civilians and adversaries with vision, hearing, and personality (cautious, courageous, aggressive, panicked). No two scenarios play out the same way.'
|
||||||
|
=> "Répliques à recul réel, physique balistique par Unreal Engine 5, civils et adversaires IA autonomes dotés de vision, ouïe et personnalité (prudent, courageux, agressif, paniqué). Aucun scénario ne se joue deux fois de la même façon.",
|
||||||
|
'Instructor-led' => 'Piloté par l\'instructeur',
|
||||||
|
"A dedicated instructor PC follows the action from any angle, manipulates the scenario in real time, displays each trainee's field of view and shot trace. Statistics roll up automatically."
|
||||||
|
=> "Un PC instructeur dédié suit l'action sous tous les angles, manipule le scénario en temps réel, affiche le champ de vision et la trace de tir de chaque stagiaire. Les statistiques remontent automatiquement.",
|
||||||
|
'AAR-ready' => 'AAR-ready',
|
||||||
|
"Replay every second of the session in top-down tactical view, free camera, or any trainee's first-person view. Pause, slow down, compare. End every drill with the data, not just the impression."
|
||||||
|
=> "Rejouez chaque seconde de la session en vue tactique zénithale, caméra libre ou première personne d'un stagiaire. Pause, ralenti, comparaison. Terminez chaque drill avec les données, pas seulement l'impression.",
|
||||||
|
|
||||||
|
'Inside the kit' => 'Le contenu du kit',
|
||||||
|
"What's in the FLEX deployment." => 'Le contenu du déploiement FLEX.',
|
||||||
|
'One trainee case per operator (laptop, headset, batteries, weapon, UWeD)'
|
||||||
|
=> 'Un coffre stagiaire par opérateur (laptop, casque, batteries, arme, UWeD)',
|
||||||
|
'One instructor PC + display' => 'Un PC instructeur + écran',
|
||||||
|
'Charging station + spare batteries' => 'Station de charge + batteries de rechange',
|
||||||
|
'Optional: haptic vest, wrist tracker, holsters, additional weapon profiles (PROSERVE+)'
|
||||||
|
=> 'Optionnel : gilet haptique, tracker de poignet, étuis, profils d\'armes additionnels (PROSERVE+)',
|
||||||
|
|
||||||
|
'Designed for tactical teams.' => 'Conçu pour les équipes tactiques.',
|
||||||
|
'Special weapons and tactics units (RAID, BRI, GIGN equivalents)'
|
||||||
|
=> 'Unités d\'intervention spéciales (RAID, BRI, équivalents GIGN)',
|
||||||
|
'Mobile gendarmerie squadrons' => 'Escadrons de gendarmerie mobile',
|
||||||
|
'National police rapid-response teams' => 'Équipes d\'intervention rapide de la police nationale',
|
||||||
|
'K9 handler training pairs' => 'Binômes maître-chien',
|
||||||
|
'Embarked teams (counter-piracy, executive protection)'
|
||||||
|
=> 'Équipes embarquées (contre-piraterie, protection rapprochée)',
|
||||||
|
|
||||||
|
'Sample scenarios' => 'Scénarios types',
|
||||||
|
'Sample team scenarios.' => 'Scénarios types en équipe.',
|
||||||
|
'Tactical progression in a residential apartment' => 'Progression tactique dans un appartement',
|
||||||
|
'Hostage rescue on an office floor' => 'Libération d\'otages dans un étage de bureaux',
|
||||||
|
'Vehicle stop with armed driver' => 'Contrôle d\'un véhicule avec conducteur armé',
|
||||||
|
'Active shooter in a supermarket' => 'Tireur actif dans un supermarché',
|
||||||
|
'Crowd containment with isolated aggressor' => 'Maintien de foule avec agresseur isolé',
|
||||||
|
'Tactical entry on a parking deck' => 'Entrée tactique sur un parking en élévation',
|
||||||
|
|
||||||
|
/* ====================== PROSERVE ACADEMY ====================== */
|
||||||
|
'For training centres, national academies, and large operational units. Up to sixteen trainees in the same free-roam scenario. Up to 900 m² of indoor space. Permanent, calibrated, supervised by a dedicated instructor cockpit.'
|
||||||
|
=> "Pour les centres de formation, les écoles nationales et les grandes unités opérationnelles. Jusqu'à seize stagiaires dans le même scénario en libre déambulation. Jusqu'à 900 m\u{B2} en intérieur. Installation permanente, calibrée, supervisée par un cockpit instructeur dédié.",
|
||||||
|
|
||||||
|
'When ACADEMY is the right fit' => 'Quand ACADEMY est adapté',
|
||||||
|
'When you need scale, not portability.' => 'Quand vous avez besoin d\'échelle, pas de portabilité.',
|
||||||
|
"ACADEMY is the right configuration when you train cohorts of 8 to 16 trainees in the same scenario, when training is the unit's daily activity, and when you have a dedicated facility you can configure once. The hardware lives in a rack. The instructor sits in a control booth. The trainees walk in, calibrate, and start."
|
||||||
|
=> "ACADEMY est la bonne configuration quand vous formez des cohortes de 8 à 16 stagiaires dans le même scénario, quand l'entraînement est l'activité quotidienne de l'unité et que vous disposez d'un local dédié configurable une fois pour toutes. Le matériel vit en baie. L'instructeur est en cabine de contrôle. Les stagiaires entrent, se calibrent et commencent.",
|
||||||
|
|
||||||
|
'What ACADEMY brings to a curriculum.' => "Ce qu'ACADEMY apporte à un cursus.",
|
||||||
|
'Up to 16 trainees, same scenario' => "Jusqu'à 16 stagiaires, même scénario",
|
||||||
|
'All trainees see each other as full-body 3D avatars whose appearance reflects their role — assault, security, breach, civilian. Communication is voice-natural and proximity-based.'
|
||||||
|
=> "Tous les stagiaires se voient en avatars 3D corps entier dont l'apparence reflète leur rôle — assaut, sécurité, brèche, civil. La communication est vocale naturelle et basée sur la proximité.",
|
||||||
|
'Modular ergonomics' => 'Ergonomie modulaire',
|
||||||
|
'Configure for one trainee or sixteen. Self-configuring software. Less than five minutes from session-end to next-session-start.'
|
||||||
|
=> "Configuration pour un ou seize stagiaires. Logiciel auto-configurable. Moins de cinq minutes entre la fin d'une session et le début de la suivante.",
|
||||||
|
'Multi-mode training' => 'Entraînement multi-modes',
|
||||||
|
'Procedural learning. Motor-skill acquisition. Repetitive drill (skill-and-drill). Deliberate fatigue training. Whatever your pedagogical model, the platform supports it.'
|
||||||
|
=> "Apprentissage procédural. Acquisition motrice. Drill répétitif (skill-and-drill). Entraînement délibéré en fatigue. Quel que soit votre modèle pédagogique, la plateforme le supporte.",
|
||||||
|
'Instructor cockpit, dedicated control room' => 'Cockpit instructeur, salle de contrôle dédiée',
|
||||||
|
"Real-time 360\u{B0} video monitoring, multi-angle tactical view, shot-trace overlay, individual statistics, scenario manipulation on the fly."
|
||||||
|
=> "Supervision vidéo 360\u{B0} en temps réel, vue tactique multi-angles, overlay des traces de tir, statistiques individuelles, manipulation du scénario à la volée.",
|
||||||
|
'Customizable scenarios' => 'Scénarios personnalisables',
|
||||||
|
"Add bespoke scenarios produced by our French studio. Replicate your local training environments, your unit's uniforms, your specific weapons profiles. Production lead time: 1 to 3 months per environment."
|
||||||
|
=> "Ajoutez des scénarios sur mesure produits dans notre studio français. Reproduisez vos environnements locaux, les uniformes de votre unité, vos profils d'armes spécifiques. Délai de production : 1 à 3 mois par environnement.",
|
||||||
|
'Permanent compute infrastructure' => 'Infrastructure de calcul permanente',
|
||||||
|
'Server rack with VR rendering machines, instructor workstation, dedicated Wi-Fi 6E network. Engineered for daily operation, redundancy, and remote support.'
|
||||||
|
=> "Baie serveur avec machines de rendu VR, poste instructeur, réseau Wi-Fi 6E dédié. Conçu pour l'opération quotidienne, la redondance et le support à distance.",
|
||||||
|
|
||||||
|
'Facility requirements' => 'Exigences du local',
|
||||||
|
'What you need on site.' => 'Ce qu\'il vous faut sur site.',
|
||||||
|
"Indoor floor footprint: from 100 m\u{B2} to 900 m\u{B2} depending on the scenarios you train (we recommend 10\u{D7}10 m for full-team dynamic CQB)"
|
||||||
|
=> "Emprise au sol : de 100 m\u{B2} à 900 m\u{B2} selon les scénarios entraînés (nous recommandons 10\u{D7}10 m pour le CQB dynamique en équipe complète)",
|
||||||
|
'Even, non-flickering lighting (no direct sunlight)' => 'Éclairage homogène et stable (pas de lumière directe du soleil)',
|
||||||
|
'Decorated walls (avoid uniformly colored surfaces — posters or markings improve tracking)'
|
||||||
|
=> "Murs habillés (éviter les surfaces uniformes — affiches ou marquages améliorent le tracking)",
|
||||||
|
"Dedicated control room (minimum 6 m\u{B2}) for instructor cockpit"
|
||||||
|
=> "Salle de contrôle dédiée (minimum 6 m\u{B2}) pour le cockpit instructeur",
|
||||||
|
"No specific HVAC requirements (unlike live-fire ranges \u{2014} no smoke, no lead, no decontamination)"
|
||||||
|
=> "Aucune exigence CVC spécifique (contrairement aux stands de tir réel \u{2014} pas de fumée, pas de plomb, pas de décontamination)",
|
||||||
|
|
||||||
|
'Designed for academies.' => 'Conçu pour les académies.',
|
||||||
|
'Police academies (national and regional)' => 'Écoles de police (nationales et régionales)',
|
||||||
|
'Gendarmerie schools' => 'Écoles de gendarmerie',
|
||||||
|
'Military training centres' => 'Centres de formation militaire',
|
||||||
|
'Firefighter training schools' => 'Écoles de sapeurs-pompiers',
|
||||||
|
'Joint inter-services training centres' => 'Centres interarmées de formation',
|
||||||
|
'Distributors and integrators looking to operate a turnkey training-as-a-service offer'
|
||||||
|
=> "Distributeurs et intégrateurs souhaitant opérer une offre training-as-a-service clé en main",
|
||||||
|
|
||||||
|
/* ====================== PROSERVE+ ====================== */
|
||||||
|
'Your environments. Your weapons. Your uniforms. Your doctrine. Our engineers, your specifications. PROSERVE+ extends every PROSERVE configuration with bespoke hardware, environments, and software, designed and produced in our French studio.'
|
||||||
|
=> "Vos environnements. Vos armes. Vos uniformes. Votre doctrine. Nos ingénieurs, vos spécifications. PROSERVE+ étend chaque configuration PROSERVE avec du matériel, des environnements et des logiciels sur mesure, conçus et produits dans notre studio français.",
|
||||||
|
|
||||||
|
'Hardware add-ons' => 'Extensions matérielles',
|
||||||
|
'Hardware extensions.' => 'Extensions matérielles.',
|
||||||
|
'Haptic vest' => 'Gilet haptique',
|
||||||
|
'Full-torso wireless haptic vest. 32 ERM motors. Renders the impact location of incoming fire and explosive proximity. Bluetooth BLE, 13.5 hours autonomy.'
|
||||||
|
=> "Gilet haptique sans fil torso complet. 32 moteurs ERM. Restitue la localisation des impacts entrants et la proximité d'explosion. Bluetooth BLE, 13,5 heures d'autonomie.",
|
||||||
|
'Wrist trackers' => 'Trackers de poignet',
|
||||||
|
'Six degree-of-freedom wrist tracking for precise hand placement, weapon retention, and grappling drills.'
|
||||||
|
=> "Tracking de poignet à six degrés de liberté pour le placement précis des mains, la rétention de l'arme et les exercices de close-combat.",
|
||||||
|
'Custom replicas' => 'Répliques sur mesure',
|
||||||
|
'Sourcing and tracker integration for replicas that go beyond the standard catalog: country-specific service weapons, less-lethal options (electroshock, OC spray, beanbag launchers), shoulder-fired support weapons, and field-specific equipment.'
|
||||||
|
=> "Sourcing et intégration de trackers pour des répliques qui dépassent le catalogue standard : armes de service spécifiques à un pays, options non-létales (électrochoc, gaz OC, lanceurs de sacs lestés), armes d'appui à l'épaule et équipements terrain spécifiques.",
|
||||||
|
|
||||||
|
'Environments and scenarios' => 'Environnements et scénarios',
|
||||||
|
'Bespoke environments and scenarios.' => 'Environnements et scénarios sur mesure.',
|
||||||
|
'All environment types are possible. We model and animate in our French studio, with our team of 3D designers and senior engineers. Lead time: from one to three months per environment, depending on complexity. One environment typically generates five to ten distinct scenarios.'
|
||||||
|
=> "Tous les types d'environnements sont possibles. Modélisation et animation dans notre studio français, avec notre équipe de designers 3D et d'ingénieurs seniors. Délai : de un à trois mois par environnement, selon la complexité. Un environnement génère typiquement cinq à dix scénarios distincts.",
|
||||||
|
'Specific city blocks (urban CQB on actual local geometry)' => 'Quartiers urbains spécifiques (CQB urbain sur géométrie locale réelle)',
|
||||||
|
'Public transit infrastructure (specific airports, stations, metros)'
|
||||||
|
=> 'Infrastructures de transport public (aéroports, gares, métros spécifiques)',
|
||||||
|
'Industrial sites (oil & gas, chemical, energy)' => 'Sites industriels (oil & gas, chimie, énergie)',
|
||||||
|
'Critical infrastructure (data centres, power plants, water treatment)'
|
||||||
|
=> 'Infrastructures critiques (data centres, centrales, traitement des eaux)',
|
||||||
|
'Government and embassy buildings' => 'Bâtiments gouvernementaux et ambassades',
|
||||||
|
'Maritime (boats, naval vessels, oil rigs)' => 'Maritime (navires, bâtiments militaires, plateformes pétrolières)',
|
||||||
|
'Aviation (commercial airliners, airfields)' => 'Aviation (avions de ligne, aérodromes)',
|
||||||
|
|
||||||
|
'Custom characters' => 'Personnages sur mesure',
|
||||||
|
'Characters in your colors.' => 'Des personnages aux couleurs de votre unité.',
|
||||||
|
'Virtual characters dressed in the uniforms of your forces, equipped with the gear of your unit. Hostile actors based on threat profiles relevant to your area of operations. Civilian behaviors localized to the cultural context of the scenario.'
|
||||||
|
=> "Personnages virtuels en uniforme de vos forces, équipés du matériel de votre unité. Acteurs hostiles fondés sur des profils de menace pertinents pour votre zone d'opération. Comportements civils localisés au contexte culturel du scénario.",
|
||||||
|
|
||||||
|
'Process' => 'Processus',
|
||||||
|
'How a PROSERVE+ project runs.' => 'Comment se déroule un projet PROSERVE+.',
|
||||||
|
'1. Discovery' => '1. Cadrage',
|
||||||
|
'We interview your operators and instructors to capture the operational reality and the pedagogical goal.'
|
||||||
|
=> "Nous interviewons vos opérateurs et instructeurs pour capter la réalité opérationnelle et l'objectif pédagogique.",
|
||||||
|
'2. Specification' => '2. Spécification',
|
||||||
|
'Joint document defining the environment, scenarios, characters, weapons, and instructor controls.'
|
||||||
|
=> "Document conjoint définissant l'environnement, les scénarios, les personnages, les armes et les contrôles instructeur.",
|
||||||
|
'3. Production' => '3. Production',
|
||||||
|
'Modeling, animation, AI behavior tuning in our French studio. Weekly review milestones.'
|
||||||
|
=> "Modélisation, animation, réglage des comportements IA dans notre studio français. Jalons de revue hebdomadaires.",
|
||||||
|
'4. Integration' => '4. Intégration',
|
||||||
|
'Software update deploys the new content to your existing PROSERVE installation.'
|
||||||
|
=> "Une mise à jour logicielle déploie le nouveau contenu sur votre installation PROSERVE existante.",
|
||||||
|
'5. Validation' => '5. Validation',
|
||||||
|
'On-site instructor training and acceptance review.'
|
||||||
|
=> "Formation des instructeurs sur site et revue de réception.",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger — fires on template_redirect (NOT init) so Bricks's
|
||||||
|
element controls are loaded (happens on 'wp' hook). Without loaded
|
||||||
|
controls, Bricks's wpml_page_builder_register_strings handler skips
|
||||||
|
every element silently and NO strings reach icl_strings → ATE empty.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_solutions'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$data_file = 'C:\\ASTERION\\GIT\\WP2026\\content-source\\solutions-data.php';
|
||||||
|
if (! file_exists($data_file)) wp_die("data file not found: $data_file");
|
||||||
|
|
||||||
|
$solutions = include $data_file;
|
||||||
|
if (! is_array($solutions)) wp_die('solutions data not array');
|
||||||
|
|
||||||
|
$parent_id = av_ensure_hub_page('solutions', 'Solutions');
|
||||||
|
$report = ["Rebuilding /solutions/ hub + child detail pages..."];
|
||||||
|
|
||||||
|
// Hub
|
||||||
|
$hub_elements = av_build_solutions_hub_elements($solutions);
|
||||||
|
$hub_write = av_write_bricks_page_elements($parent_id, $hub_elements);
|
||||||
|
$report[] = " HUB → page #$parent_id (rebuilt) — " . count($hub_elements)
|
||||||
|
. " elements (json={$hub_write['json_len']}, stored={$hub_write['stored_len']})";
|
||||||
|
|
||||||
|
// Detail pages
|
||||||
|
foreach ($solutions as $slug => $d) {
|
||||||
|
$page = av_upsert_detail_page($slug, 'solutions', $d);
|
||||||
|
$report[] = " $slug → page #{$page['id']} ({$page['action']}) — {$page['element_count']} elements (json={$page['json_len']}, stored={$page['stored_len']})";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Solutions hub : hero + 4-card grid (tier comparison) + closer CTA
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_build_solutions_hub_elements($solutions) {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// HERO
|
||||||
|
$els = array_merge($els, av_build_hero('shub', [
|
||||||
|
'eyebrow' => 'PROSERVE platform',
|
||||||
|
'tagline' => "Four tiers. One platform. Train where you are.",
|
||||||
|
'hero_lead' => "From the solo officer in a 9 m\u{B2} corner to a 16-trainee free-roam academy : PROSERVE\u{2122} scales without changing software. Same physics. Same AI. Same instructor logic. Pick the configuration that fits your context — upgrade or extend later.",
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'Book a demo', 'href' => '/request-demo/', 'variant' => 'primary'],
|
||||||
|
['label' => 'Request a T&E Kit', 'href' => '/request-te-kit/', 'variant' => 'secondary'],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// 4-card grid — one card per tier
|
||||||
|
$cards_section = [
|
||||||
|
'eyebrow' => '',
|
||||||
|
'title' => 'Pick the right configuration for your unit.',
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
foreach ($solutions as $slug => $d) {
|
||||||
|
$cards_section['items'][] = [
|
||||||
|
'title' => $d['title'],
|
||||||
|
'body' => ($d['eyebrow'] ?? '') . " \u{2014} " . ($d['tagline'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$els = array_merge($els, av_build_cards_section('shubgrid', 0, $cards_section, 'var(--color-bg-subtle)'));
|
||||||
|
|
||||||
|
// Append "Learn more →" link in each card (post-build patch)
|
||||||
|
$i = 0;
|
||||||
|
foreach ($solutions as $slug => $d) {
|
||||||
|
$kid = "kshubgridc0x{$i}";
|
||||||
|
$lid = "lnshubgridc0x{$i}";
|
||||||
|
$els[] = av_text_link($lid, $kid, "Learn more \u{2192}", "/solutions/$slug/");
|
||||||
|
foreach ($els as &$el) {
|
||||||
|
if (($el['id'] ?? '') === $kid) {
|
||||||
|
$el['children'][] = $lid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($el);
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLOSER CTA
|
||||||
|
$els = array_merge($els, av_build_closer_cta('shub'));
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
306
tools/av-generate-technology.php
Normal file
306
tools/av-generate-technology.php
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate technology pages (one-shot)
|
||||||
|
* Description: /?av_generate_technology=1 reads content-source/technology-data.php
|
||||||
|
* and creates/updates : hub /technology/ + 6 child pages (vr-hardware,
|
||||||
|
* software-ai, weapons-tracking, scenarios, instructor-cockpit,
|
||||||
|
* after-action-review). Idempotent.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Register Technology-specific FR strings into the shared dict.
|
||||||
|
Brand names (PROSERVE, PROSERVE+, UWeD, Asterion VR) verbatim.
|
||||||
|
Hardware specs (numbers, MHz, GB, mm) often need no translation.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, av_technology_fr_dict());
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_technology_fr_dict() {
|
||||||
|
return [
|
||||||
|
/* ============================ HUB ============================ */
|
||||||
|
'Technology' => 'Technologie',
|
||||||
|
'PROSERVE platform' => 'Plateforme PROSERVE',
|
||||||
|
"Six pillars. One coherent platform." => "Six piliers. Une plateforme cohérente.",
|
||||||
|
"VR hardware, autonomous AI, real weapons with universal tracking, fifty-plus scenarios, instructor cockpit, frame-level after-action review. Each pillar engineered to support training under operational conditions \u{2014} every day, every shift."
|
||||||
|
=> "Matériel VR, IA autonome, vraies armes avec tracking universel, plus de cinquante scénarios, cockpit instructeur, debriefing image par image. Chaque pilier conçu pour soutenir l'entraînement en conditions opérationnelles \u{2014} chaque jour, chaque service.",
|
||||||
|
"Explore each pillar of the platform." => "Explorez chaque pilier de la plateforme.",
|
||||||
|
|
||||||
|
/* -------- Card titles + concatenated bodies (hub) -------- */
|
||||||
|
'VR Hardware' => 'Matériel VR',
|
||||||
|
'Software & AI' => 'Logiciel & IA',
|
||||||
|
'Weapons & Tracking' => 'Armes & Tracking',
|
||||||
|
'Scenarios Library' => 'Bibliothèque de scénarios',
|
||||||
|
'Scenarios' => 'Scénarios',
|
||||||
|
'Instructor Cockpit' => 'Cockpit instructeur',
|
||||||
|
'After-Action Review' => 'Debriefing (AAR)',
|
||||||
|
|
||||||
|
'Wireless. 5K. Engineered for daily operational use.'
|
||||||
|
=> 'Sans fil. 5K. Conçu pour l\'usage opérationnel quotidien.',
|
||||||
|
'Behavior, not scripts.'
|
||||||
|
=> 'Du comportement, pas du script.',
|
||||||
|
'The weapon you train with is the weapon you carry.'
|
||||||
|
=> 'L\'arme avec laquelle tu t\'entraînes est celle que tu portes.',
|
||||||
|
'Eleven environments. More than fifty scenarios. New ones every quarter.'
|
||||||
|
=> 'Onze environnements. Plus de cinquante scénarios. De nouveaux chaque trimestre.',
|
||||||
|
'The cockpit between the simulation and the lesson.'
|
||||||
|
=> 'Le cockpit entre la simulation et la leçon.',
|
||||||
|
'Every drill ends with data. Not impressions.'
|
||||||
|
=> 'Chaque exercice se termine avec des données. Pas des impressions.',
|
||||||
|
|
||||||
|
// Card body concatenations from hub builder (eyebrow — tagline)
|
||||||
|
"VR Hardware \u{2014} Wireless. 5K. Engineered for daily operational use."
|
||||||
|
=> "Matériel VR \u{2014} Sans fil. 5K. Conçu pour l'usage opérationnel quotidien.",
|
||||||
|
"Software & AI \u{2014} Behavior, not scripts."
|
||||||
|
=> "Logiciel & IA \u{2014} Du comportement, pas du script.",
|
||||||
|
"Weapons & Tracking \u{2014} The weapon you train with is the weapon you carry."
|
||||||
|
=> "Armes & Tracking \u{2014} L'arme avec laquelle tu t'entraînes est celle que tu portes.",
|
||||||
|
"Scenarios Library \u{2014} Eleven environments. More than fifty scenarios. New ones every quarter."
|
||||||
|
=> "Bibliothèque de scénarios \u{2014} Onze environnements. Plus de cinquante scénarios. De nouveaux chaque trimestre.",
|
||||||
|
"Instructor Cockpit \u{2014} The cockpit between the simulation and the lesson."
|
||||||
|
=> "Cockpit instructeur \u{2014} Le cockpit entre la simulation et la leçon.",
|
||||||
|
"After-Action Review \u{2014} Every drill ends with data. Not impressions."
|
||||||
|
=> "Debriefing (AAR) \u{2014} Chaque exercice se termine avec des données. Pas des impressions.",
|
||||||
|
|
||||||
|
/* ====================== VR Hardware ====================== */
|
||||||
|
"PROSERVE's hardware was selected for one purpose: to disappear inside the training. The headset is fully wireless, with hot-swappable batteries. The rendering compute lives in a laptop, not a tower. Wi-Fi 6E delivers headset visuals at 90 Hz, with no cable to trip on, no fixed install to maintain."
|
||||||
|
=> "Le matériel PROSERVE a été choisi dans un seul but : disparaître pendant l'entraînement. Le casque est entièrement sans fil, avec batteries échangeables à chaud. Le calcul de rendu vit dans un portable, pas une tour. Le Wi-Fi 6E délivre les visuels du casque à 90 Hz, sans câble qui traîne, sans installation fixe à maintenir.",
|
||||||
|
|
||||||
|
'The headset' => 'Le casque',
|
||||||
|
'Headset specifications.' => 'Spécifications du casque.',
|
||||||
|
'The rendering compute' => 'L\'unité de calcul',
|
||||||
|
'Compute specifications.' => 'Spécifications de calcul.',
|
||||||
|
'The wireless network' => 'Le réseau sans fil',
|
||||||
|
'Wireless network specifications.' => 'Spécifications réseau sans fil.',
|
||||||
|
|
||||||
|
'Why this matters' => 'Pourquoi ça compte',
|
||||||
|
'Why this hardware choice.' => 'Pourquoi ce choix matériel.',
|
||||||
|
"Wireless 5K visuals at 90 Hz remove the most common immersion-breaker in tactical VR: cable management. Hot-swappable batteries and laptop-class compute let a deployment unfold on any flat surface, without an installer. Inside-out tracking removes the dependency on calibrated external base stations. The whole rig fits in a 20 kg waterproof case \u{2014} and recovers from a fall on concrete. Specifications evolve as VR hardware advances: PROSERVE customers receive hardware refreshes within their support window, at no additional cost."
|
||||||
|
=> "Les visuels 5K sans fil à 90 Hz éliminent le plus fréquent rupteur d'immersion en VR tactique : la gestion des câbles. Batteries échangeables à chaud et calcul portable permettent un déploiement sur toute surface plane, sans installateur. Le tracking inside-out élimine la dépendance aux stations externes calibrées. L'ensemble tient dans un coffre étanche de 20 kg \u{2014} et résiste à une chute sur béton. Les spécifications évoluent avec le matériel VR : les clients PROSERVE reçoivent les mises à niveau matériel dans leur fenêtre de support, sans coût supplémentaire.",
|
||||||
|
|
||||||
|
/* ====================== Software & AI ====================== */
|
||||||
|
"PROSERVE's software stack is built on Unreal Engine 5 and a proprietary AI engine that gives every civilian and adversary in the simulation an internal state \u{2014} vision, hearing, courage, caution, aggression. The same scenario, with the same starting conditions, never plays out the same way twice."
|
||||||
|
=> "La pile logicielle PROSERVE est construite sur Unreal Engine 5 et un moteur IA propriétaire qui donne à chaque civil et adversaire de la simulation un état interne \u{2014} vision, ouïe, courage, prudence, agressivité. Le même scénario, avec les mêmes conditions de départ, ne se joue jamais deux fois pareil.",
|
||||||
|
|
||||||
|
'Powered by Unreal Engine 5' => 'Propulsé par Unreal Engine 5',
|
||||||
|
'Photoreal real-time rendering.' => 'Rendu photoréaliste en temps réel.',
|
||||||
|
'Real-time photoreal rendering. Volumetric lighting. Dynamic global illumination. Material-aware ballistic surfaces. Day, night, and emergency-light conditions. NVIDIA RTX-accelerated effects.'
|
||||||
|
=> "Rendu photoréaliste en temps réel. Éclairage volumétrique. Illumination globale dynamique. Surfaces balistiques sensibles aux matériaux. Conditions de jour, nuit, et éclairage d'urgence. Effets accélérés NVIDIA RTX.",
|
||||||
|
|
||||||
|
'Autonomous AI agents' => 'Agents IA autonomes',
|
||||||
|
'What the agents perceive and do.' => 'Ce que les agents perçoivent et font.',
|
||||||
|
'Vision' => 'Vision',
|
||||||
|
'Line-of-sight, peripheral awareness, recognition under stress.'
|
||||||
|
=> "Ligne de mire, vision périphérique, reconnaissance sous stress.",
|
||||||
|
'Hearing' => 'Ouïe',
|
||||||
|
'Gunfire, footsteps, voice, environmental noise.'
|
||||||
|
=> "Tirs, pas, voix, bruits ambiants.",
|
||||||
|
'Memory' => 'Mémoire',
|
||||||
|
'They remember where they last saw a threat.'
|
||||||
|
=> "Ils se souviennent où ils ont vu une menace pour la dernière fois.",
|
||||||
|
'Movement' => 'Déplacement',
|
||||||
|
'Move freely through complex environments \u{2014} staircases, corners, multi-room interiors.'
|
||||||
|
=> "Se déplacent librement dans des environnements complexes \u{2014} escaliers, angles, intérieurs multi-pièces.",
|
||||||
|
'Engagement' => 'Engagement',
|
||||||
|
'Take cover, peek, return fire, retreat. Change target dynamically based on threat assessment.'
|
||||||
|
=> "Se mettent à couvert, observent, ripostent, se retirent. Changent de cible dynamiquement selon l'évaluation de la menace.",
|
||||||
|
'Coordination' => 'Coordination',
|
||||||
|
'Coordinate with other agents (squad behavior).'
|
||||||
|
=> "Se coordonnent avec d'autres agents (comportement d'escouade).",
|
||||||
|
|
||||||
|
'Configurable personality' => 'Personnalité configurable',
|
||||||
|
'Each agent has an internal state.' => 'Chaque agent a un état interne.',
|
||||||
|
'Each agent receives a personality profile blending courage, caution, and aggression. A panicked civilian flees in a different direction from a hostile actor under fire. A coordinated cell behaves differently from a lone aggressor. Instructors can preset, randomize, or override personality on the fly from the cockpit.'
|
||||||
|
=> "Chaque agent reçoit un profil de personnalité combinant courage, prudence et agressivité. Un civil paniqué fuit dans une direction différente d'un acteur hostile sous le feu. Une cellule coordonnée se comporte différemment d'un agresseur isolé. Les instructeurs peuvent prédéfinir, randomiser ou outrepasser la personnalité à la volée depuis le cockpit.",
|
||||||
|
|
||||||
|
'Real-time ballistic simulation' => 'Simulation balistique en temps réel',
|
||||||
|
'Physics-driven ballistics.' => 'Balistique pilotée par la physique.',
|
||||||
|
|
||||||
|
'Local-only operation' => 'Fonctionnement strictement local',
|
||||||
|
'No cloud. No telemetry. No exposure.' => 'Pas de cloud. Pas de télémétrie. Pas d\'exposition.',
|
||||||
|
|
||||||
|
/* ====================== Weapons & Tracking ====================== */
|
||||||
|
'Most VR training tools give the trainee a controller shaped like a pistol. PROSERVE gives the trainee a gas-blowback replica or a real service weapon firing blanks, fitted with our Universal Weapon Digitizer. The weight, the recoil, the sound, the iron-sight alignment \u{2014} all coherent with the carried weapon. Muscle memory is what we are paying to train.'
|
||||||
|
=> "La plupart des outils VR donnent au stagiaire une manette en forme de pistolet. PROSERVE donne une réplique gas-blowback ou une vraie arme de service à blanc, équipée de notre Universal Weapon Digitizer. Le poids, le recul, le son, l'alignement des organes de visée \u{2014} tout est cohérent avec l'arme portée. C'est la mémoire musculaire que nous payons pour entraîner.",
|
||||||
|
|
||||||
|
'The Universal Weapon Digitizer (UWeD)' => 'L\'Universal Weapon Digitizer (UWeD)',
|
||||||
|
'UWeD specifications.' => 'Spécifications UWeD.',
|
||||||
|
'UWeD is our proprietary weapon module. It attaches to any Picatinny rail or to compatible mounts, providing position and orientation tracking with recoil-induced perturbation correction.'
|
||||||
|
=> "L'UWeD est notre module d'arme propriétaire. Il s'attache à n'importe quel rail Picatinny ou à des montures compatibles, fournissant le tracking de position et d'orientation avec correction des perturbations dues au recul.",
|
||||||
|
|
||||||
|
'Dual-channel shot detection' => 'Détection de tir bicanale',
|
||||||
|
'No false positives in cooperative fire.' => 'Pas de faux positifs en tir coopératif.',
|
||||||
|
"Every shot is detected by combining sound and recoil signatures. This dual-channel detection avoids false positives from a teammate's shot fired nearby \u{2014} a critical safeguard for cooperative training where multiple weapons fire in the same room."
|
||||||
|
=> "Chaque tir est détecté en combinant signatures sonore et de recul. Cette détection bicanale évite les faux positifs causés par un tir d'équipier proche \u{2014} garantie critique pour l'entraînement coopératif où plusieurs armes tirent dans la même pièce.",
|
||||||
|
|
||||||
|
'Standard weapon catalog' => 'Catalogue d\'armes standard',
|
||||||
|
'Compatible weapons (non-exhaustive).' => 'Armes compatibles (liste non exhaustive).',
|
||||||
|
'Additional weapons supported on request via PROSERVE+.' => 'Autres armes disponibles sur demande via PROSERVE+.',
|
||||||
|
'Handguns' => 'Armes de poing',
|
||||||
|
'Glock 17 Gen 5 GBB, Glock 19 GBB / CO2, Beretta M9 GBB, Heckler & Koch P45 (Umarex), Heckler & Koch VP9 (Umarex), ELITE IA GBB.'
|
||||||
|
=> 'Glock 17 Gen 5 GBB, Glock 19 GBB / CO2, Beretta M9 GBB, Heckler & Koch P45 (Umarex), Heckler & Koch VP9 (Umarex), ELITE IA GBB.',
|
||||||
|
'Rifles & SMGs' => 'Fusils & PM',
|
||||||
|
'HK416 (Umarex), HK417 (Umarex), MP7 (Umarex), PMX, Colt M4.'
|
||||||
|
=> 'HK416 (Umarex), HK417 (Umarex), MP7 (Umarex), PMX, Colt M4.',
|
||||||
|
'Operating costs' => 'Coûts opérationnels',
|
||||||
|
"CO2 refill: ~\u{20AC}0.25 per cartridge, 30-40 shots per refill. Gas refill: ~\u{20AC}8 per cartridge, 600-800 shots per refill. No live ammunition. No range fees. No decontamination."
|
||||||
|
=> "Recharge CO2 : ~0,25 \u{20AC} par cartouche, 30-40 tirs par recharge. Recharge gaz : ~8 \u{20AC} par cartouche, 600-800 tirs par recharge. Pas de munitions réelles. Pas de frais de stand. Pas de décontamination.",
|
||||||
|
|
||||||
|
/* ====================== Scenarios ====================== */
|
||||||
|
'The PROSERVE scenarios library covers the operational reality of contemporary security forces \u{2014} from technical marksmanship to multi-room cooperative CQB. Every scenario was specified with serving and former operators. Updates roll out four times a year.'
|
||||||
|
=> "La bibliothèque de scénarios PROSERVE couvre la réalité opérationnelle des forces de sécurité contemporaines \u{2014} du tir de précision technique au CQB coopératif multi-pièces. Chaque scénario a été spécifié avec des opérateurs en service et anciens. Les mises à jour sortent quatre fois par an.",
|
||||||
|
|
||||||
|
'Static training' => 'Entraînement statique',
|
||||||
|
'Marksmanship & decision.' => 'Précision & décision.',
|
||||||
|
'Dynamic training' => 'Entraînement dynamique',
|
||||||
|
'Mobile, decision-making.' => 'Mobile, décisionnel.',
|
||||||
|
'Multi-trainee training' => 'Entraînement multi-stagiaires',
|
||||||
|
'Tactical progression.' => 'Progression tactique.',
|
||||||
|
|
||||||
|
'Variability' => 'Variabilité',
|
||||||
|
'Same starting conditions, different outcomes.' => 'Mêmes conditions de départ, issues différentes.',
|
||||||
|
'Each scenario has between five and eight pre-built variations. AI behavior introduces additional variability \u{2014} same starting conditions, different outcomes. Day, night, and emergency-light conditions are toggleable on most scenarios.'
|
||||||
|
=> "Chaque scénario a entre cinq et huit variations pré-construites. Le comportement de l'IA introduit une variabilité supplémentaire \u{2014} mêmes conditions de départ, issues différentes. Les conditions jour, nuit et éclairage d'urgence sont activables sur la plupart des scénarios.",
|
||||||
|
|
||||||
|
'Custom scenarios' => 'Scénarios sur mesure',
|
||||||
|
'PROSERVE+ produces bespoke scenarios on demand.' => 'PROSERVE+ produit des scénarios sur mesure à la demande.',
|
||||||
|
'Your local geography, your operational doctrine, your area-specific threats. Production lead time: 1 to 3 months per environment.'
|
||||||
|
=> "Votre géographie locale, votre doctrine opérationnelle, vos menaces spécifiques. Délai de production : 1 à 3 mois par environnement.",
|
||||||
|
'Discuss a custom scenario' => 'Discuter d\'un scénario sur mesure',
|
||||||
|
|
||||||
|
/* ====================== Instructor Cockpit ====================== */
|
||||||
|
"PROSERVE's instructor cockpit is what separates a simulator from a training tool. From a dedicated workstation, the instructor orchestrates every detail of the scenario in real time \u{2014} without pausing it, without breaking the trainee's immersion, with complete tactical and pedagogical visibility."
|
||||||
|
=> "Le cockpit instructeur PROSERVE est ce qui sépare un simulateur d'un outil d'entraînement. Depuis un poste dédié, l'instructeur orchestre chaque détail du scénario en temps réel \u{2014} sans le mettre en pause, sans rompre l'immersion du stagiaire, avec une visibilité tactique et pédagogique complète.",
|
||||||
|
|
||||||
|
'Real-time supervision' => 'Supervision en temps réel',
|
||||||
|
'See what no one else can.' => 'Voir ce que personne d\'autre ne peut.',
|
||||||
|
'Walk through walls' => 'Traverser les murs',
|
||||||
|
'Free camera control. The instructor sees the engagement from any angle: top-down tactical, third-person trail, fixed positions, or directly through walls.'
|
||||||
|
=> "Contrôle de caméra libre. L'instructeur voit l'engagement sous tout angle : tactique zénithal, suivi en troisième personne, positions fixes, ou directement à travers les murs.",
|
||||||
|
"Switch into any trainee's view" => 'Basculer dans la vue de n\'importe quel stagiaire',
|
||||||
|
'First-person view of any trainee, in real time. See exactly what they see \u{2014} and what they failed to see.'
|
||||||
|
=> "Vue à la première personne de n'importe quel stagiaire, en temps réel. Voyez exactement ce qu'ils voient \u{2014} et ce qu'ils n'ont pas vu.",
|
||||||
|
'Toggle overlays' => 'Activer les overlays',
|
||||||
|
'Shot traces (with hit/miss color coding). Field of view of each trainee. Threat highlighting (display all hostile actors as pulsing markers, useful for instructor briefing).'
|
||||||
|
=> "Traces de tir (avec code couleur impact/raté). Champ de vision de chaque stagiaire. Surlignage des menaces (affiche tous les acteurs hostiles comme marqueurs pulsants, utile pour le briefing instructeur).",
|
||||||
|
|
||||||
|
'Scenario manipulation in real time' => 'Manipulation du scénario en temps réel',
|
||||||
|
'Reshape the drill without pausing it.' => 'Refaçonnez l\'exercice sans l\'interrompre.',
|
||||||
|
|
||||||
|
'Statistics on the fly' => 'Statistiques à la volée',
|
||||||
|
'Live data during the session.' => 'Données en direct pendant la session.',
|
||||||
|
|
||||||
|
'Multi-instructor support' => 'Support multi-instructeurs',
|
||||||
|
'Multiple instructors, one cockpit.' => 'Plusieurs instructeurs, un cockpit.',
|
||||||
|
'On PROSERVE ACADEMY configurations, multiple instructors can supervise simultaneously \u{2014} one in command of scenario manipulation, one focused on individual trainee follow-up, one running statistics review.'
|
||||||
|
=> "Sur les configurations PROSERVE ACADEMY, plusieurs instructeurs peuvent superviser simultanément \u{2014} un aux commandes de la manipulation du scénario, un focalisé sur le suivi individuel, un sur la revue des statistiques.",
|
||||||
|
|
||||||
|
/* ====================== After-Action Review ====================== */
|
||||||
|
"Conventional after-action review depends on instructor recall and trainee self-assessment \u{2014} both unreliable. PROSERVE's AAR records every event of every session and lets you replay it from any angle, at any speed, with overlays the human eye couldn't catch in real time."
|
||||||
|
=> "Le debriefing conventionnel dépend du souvenir de l'instructeur et de l'auto-évaluation du stagiaire \u{2014} les deux peu fiables. L'AAR PROSERVE enregistre chaque événement de chaque session et permet de le rejouer sous tout angle, à toute vitesse, avec des overlays que l'\u{153}il humain ne pouvait capter en temps réel.",
|
||||||
|
|
||||||
|
'Replay from any angle' => 'Rejouer sous tout angle',
|
||||||
|
'Camera modes.' => 'Modes caméra.',
|
||||||
|
'Overlays' => 'Overlays',
|
||||||
|
'See what real-time observation missed.' => 'Voir ce que l\'observation en temps réel a manqué.',
|
||||||
|
|
||||||
|
'Per-trainee statistics' => 'Statistiques par stagiaire',
|
||||||
|
'Individual performance data.' => 'Données de performance individuelle.',
|
||||||
|
|
||||||
|
'Unit-level dashboards' => 'Tableaux de bord par unité',
|
||||||
|
'Aggregated metrics for command.' => 'Métriques agrégées pour le commandement.',
|
||||||
|
|
||||||
|
'Comparison and benchmarking' => 'Comparaison et benchmark',
|
||||||
|
'The data is the curriculum.' => 'La donnée est le programme.',
|
||||||
|
"Compare a recruit's performance to the unit median. Compare today's drill to last month's. Compare two squads on the same scenario. The data is the curriculum."
|
||||||
|
=> "Comparez la performance d'une recrue à la médiane de l'unité. Comparez l'exercice du jour à celui du mois dernier. Comparez deux escouades sur le même scénario. La donnée est le programme.",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger — template_redirect (Bricks controls loaded by then)
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_technology'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$data_file = 'C:\\ASTERION\\GIT\\WP2026\\content-source\\technology-data.php';
|
||||||
|
if (! file_exists($data_file)) wp_die("data file not found: $data_file");
|
||||||
|
|
||||||
|
$technology = include $data_file;
|
||||||
|
if (! is_array($technology)) wp_die('technology data not array');
|
||||||
|
|
||||||
|
$parent_id = av_ensure_hub_page('technology', 'Technology');
|
||||||
|
$report = ["Rebuilding /technology/ hub + child detail pages..."];
|
||||||
|
|
||||||
|
// Hub
|
||||||
|
$hub_elements = av_build_technology_hub_elements($technology);
|
||||||
|
$hub_write = av_write_bricks_page_elements($parent_id, $hub_elements);
|
||||||
|
$report[] = " HUB → page #$parent_id (rebuilt) — " . count($hub_elements)
|
||||||
|
. " elements (json={$hub_write['json_len']}, stored={$hub_write['stored_len']})";
|
||||||
|
|
||||||
|
// Detail pages
|
||||||
|
foreach ($technology as $slug => $d) {
|
||||||
|
$page = av_upsert_detail_page($slug, 'technology', $d);
|
||||||
|
$report[] = " $slug → page #{$page['id']} ({$page['action']}) — {$page['element_count']} elements (json={$page['json_len']}, stored={$page['stored_len']})";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Technology hub : hero + 6-card grid (one per pillar) + closer CTA
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_build_technology_hub_elements($technology) {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// HERO
|
||||||
|
$els = array_merge($els, av_build_hero('thub', [
|
||||||
|
'eyebrow' => 'PROSERVE platform',
|
||||||
|
'tagline' => "Six pillars. One coherent platform.",
|
||||||
|
'hero_lead' => "VR hardware, autonomous AI, real weapons with universal tracking, fifty-plus scenarios, instructor cockpit, frame-level after-action review. Each pillar engineered to support training under operational conditions \u{2014} every day, every shift.",
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'Book a demo', 'href' => '/request-demo/', 'variant' => 'primary'],
|
||||||
|
['label' => 'Request a T&E Kit', 'href' => '/request-te-kit/', 'variant' => 'secondary'],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// 6-card grid — one card per pillar (will render 3+3 thanks to count=6 case)
|
||||||
|
$cards_section = [
|
||||||
|
'eyebrow' => '',
|
||||||
|
'title' => 'Explore each pillar of the platform.',
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
foreach ($technology as $slug => $d) {
|
||||||
|
$cards_section['items'][] = [
|
||||||
|
'title' => $d['title'],
|
||||||
|
'body' => ($d['eyebrow'] ?? '') . " \u{2014} " . ($d['tagline'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$els = array_merge($els, av_build_cards_section('thubgrid', 0, $cards_section, 'var(--color-bg-subtle)'));
|
||||||
|
|
||||||
|
// Append "Learn more →" link in each card
|
||||||
|
$i = 0;
|
||||||
|
foreach ($technology as $slug => $d) {
|
||||||
|
$kid = "kthubgridc0x{$i}";
|
||||||
|
$lid = "lnthubgridc0x{$i}";
|
||||||
|
$els[] = av_text_link($lid, $kid, "Learn more \u{2192}", "/technology/$slug/");
|
||||||
|
foreach ($els as &$el) {
|
||||||
|
if (($el['id'] ?? '') === $kid) {
|
||||||
|
$el['children'][] = $lid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($el);
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLOSER CTA
|
||||||
|
$els = array_merge($els, av_build_closer_cta('thub'));
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
61
tools/av-header-dropdown-style.php
Normal file
61
tools/av-header-dropdown-style.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Header dropdown styling overrides
|
||||||
|
* Description: Injects a small targeted stylesheet for the Bricks nav-menu
|
||||||
|
* dropdown (.bricks-nav-menu .sub-menu) so submenu items don't inherit
|
||||||
|
* the bloated default heights. Bricks's nav-menu controls don't expose
|
||||||
|
* all the relevant selectors (e.g. line-height, item li margins), so we
|
||||||
|
* patch via wp_head() directly.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('wp_head', function () {
|
||||||
|
?>
|
||||||
|
<style id="av-header-dropdown">
|
||||||
|
/* Sub-menu wrapper : compact + branded */
|
||||||
|
.bricks-nav-menu .sub-menu {
|
||||||
|
padding: 0.5rem 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
min-width: 16rem;
|
||||||
|
background: var(--color-brand-navy) !important;
|
||||||
|
border: 1px solid rgba(201, 164, 90, 0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
/* Each li : no extra spacing */
|
||||||
|
.bricks-nav-menu .sub-menu > li {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
list-style: none;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
}
|
||||||
|
/* Each link : tight padding, branded color */
|
||||||
|
.bricks-nav-menu .sub-menu > li > a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.55rem 1.25rem !important;
|
||||||
|
color: var(--color-text-on-dark) !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.15s ease, background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
/* Hover : gold text + subtle bg */
|
||||||
|
.bricks-nav-menu .sub-menu > li:hover > a,
|
||||||
|
.bricks-nav-menu .sub-menu > li > a:hover,
|
||||||
|
.bricks-nav-menu .sub-menu > li > a:focus {
|
||||||
|
color: var(--color-brand-gold) !important;
|
||||||
|
background-color: rgba(201, 164, 90, 0.08);
|
||||||
|
}
|
||||||
|
/* Current page marker */
|
||||||
|
.bricks-nav-menu .sub-menu > li.current-menu-item > a {
|
||||||
|
color: var(--color-brand-gold) !important;
|
||||||
|
}
|
||||||
|
/* Remove default Bricks chevron clutter inside the submenu */
|
||||||
|
.bricks-nav-menu .sub-menu .brx-submenu-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}, 999);
|
||||||
182
tools/av-lang-switcher.php
Normal file
182
tools/av-lang-switcher.php
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Custom language switcher (flag dropdown)
|
||||||
|
* Description: Replaces WPML's default language selector output with a compact
|
||||||
|
* flag-based dropdown. Renders via wpml_add_language_selector action
|
||||||
|
* (the same action Bricks's wpml-language-switcher element fires), so no
|
||||||
|
* change to the Header template is needed — just swap the action handler.
|
||||||
|
*
|
||||||
|
* Output : current language flag + 2-letter code button + chevron, with
|
||||||
|
* a CSS-only hover dropdown listing other available languages.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
1. Remove WPML's default language selector action handler
|
||||||
|
and register our own.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('init', function () {
|
||||||
|
// WPML registers the default selector internally. Remove ALL existing
|
||||||
|
// callbacks attached to wpml_add_language_selector, then attach ours.
|
||||||
|
remove_all_actions('wpml_add_language_selector');
|
||||||
|
add_action('wpml_add_language_selector', 'av_render_lang_switcher');
|
||||||
|
}, 99);
|
||||||
|
|
||||||
|
function av_render_lang_switcher() {
|
||||||
|
$languages = apply_filters('wpml_active_languages', null, 'skip_missing=0');
|
||||||
|
if (empty($languages)) return;
|
||||||
|
|
||||||
|
// Find current language
|
||||||
|
$current_code = apply_filters('wpml_current_language', null);
|
||||||
|
$current = $languages[$current_code] ?? reset($languages);
|
||||||
|
$others = [];
|
||||||
|
foreach ($languages as $code => $lang) {
|
||||||
|
if ($code !== $current_code) $others[] = $lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag emoji per language code (extend as needed)
|
||||||
|
$flags = [
|
||||||
|
'en' => "\u{1F1EC}\u{1F1E7}", // 🇬🇧
|
||||||
|
'fr' => "\u{1F1EB}\u{1F1F7}", // 🇫🇷
|
||||||
|
'de' => "\u{1F1E9}\u{1F1EA}",
|
||||||
|
'es' => "\u{1F1EA}\u{1F1F8}",
|
||||||
|
'it' => "\u{1F1EE}\u{1F1F9}",
|
||||||
|
];
|
||||||
|
|
||||||
|
$current_flag = $flags[$current['language_code']] ?? "\u{1F310}";
|
||||||
|
$current_label = strtoupper($current['language_code']);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="av-lang-switcher" tabindex="0" aria-haspopup="true">
|
||||||
|
<span class="av-lang-current">
|
||||||
|
<span class="av-lang-flag" aria-hidden="true"><?php echo esc_html($current_flag); ?></span>
|
||||||
|
<span class="av-lang-code"><?php echo esc_html($current_label); ?></span>
|
||||||
|
<svg class="av-lang-chevron" width="10" height="6" viewBox="0 0 10 6" fill="none" aria-hidden="true">
|
||||||
|
<path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<?php if (! empty($others)): ?>
|
||||||
|
<ul class="av-lang-list" role="menu">
|
||||||
|
<?php foreach ($others as $lang):
|
||||||
|
$flag = $flags[$lang['language_code']] ?? "\u{1F310}";
|
||||||
|
$url = $lang['url'] ?: home_url('/' . $lang['language_code'] . '/');
|
||||||
|
// Use native_name (Français / English / Deutsch...) — universally recognizable
|
||||||
|
// and stays short for the dropdown.
|
||||||
|
$name = $lang['native_name'] ?: $lang['translated_name'] ?: strtoupper($lang['language_code']);
|
||||||
|
?>
|
||||||
|
<li role="none">
|
||||||
|
<a href="<?php echo esc_url($url); ?>" hreflang="<?php echo esc_attr($lang['language_code']); ?>" role="menuitem">
|
||||||
|
<span class="av-lang-flag" aria-hidden="true"><?php echo esc_html($flag); ?></span>
|
||||||
|
<span class="av-lang-name"><?php echo esc_html($name); ?></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
2. Inline CSS — compact, branded, CSS-only dropdown on hover/focus
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('wp_head', function () {
|
||||||
|
?>
|
||||||
|
<style id="av-lang-switcher-styles">
|
||||||
|
.av-lang-switcher {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.av-lang-current {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem 0.65rem;
|
||||||
|
color: var(--color-text-on-dark, #fff);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
.av-lang-switcher:hover .av-lang-current,
|
||||||
|
.av-lang-switcher:focus-within .av-lang-current {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-color: rgba(201, 164, 90, 0.4);
|
||||||
|
}
|
||||||
|
.av-lang-flag {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.av-lang-code {
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.av-lang-chevron {
|
||||||
|
margin-left: 0.15rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.av-lang-switcher:hover .av-lang-chevron,
|
||||||
|
.av-lang-switcher:focus-within .av-lang-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
/* Dropdown list — hidden by default, shown on hover/focus */
|
||||||
|
.av-lang-list {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.25rem);
|
||||||
|
right: 0;
|
||||||
|
min-width: 11rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
list-style: none;
|
||||||
|
background: var(--color-brand-navy, #0B1F3A);
|
||||||
|
border: 1px solid rgba(201, 164, 90, 0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s 0.18s;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.av-lang-switcher:hover .av-lang-list,
|
||||||
|
.av-lang-switcher:focus-within .av-lang-list {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
.av-lang-list a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
color: var(--color-text-on-dark, #fff);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.15s ease, background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
.av-lang-list a:hover,
|
||||||
|
.av-lang-list a:focus {
|
||||||
|
color: var(--color-brand-gold, #C9A45A);
|
||||||
|
background: rgba(201, 164, 90, 0.08);
|
||||||
|
}
|
||||||
|
.av-lang-list .av-lang-flag {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
/* Mobile : ensure switcher stays visible in mobile menu context */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.av-lang-list {
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}, 999);
|
||||||
34
tools/av-list-bullet-style.php
Normal file
34
tools/av-list-bullet-style.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Vertical gold bullet for list items
|
||||||
|
* Description: Replaces the "— " dash prefix with a strong vertical gold bar
|
||||||
|
* accent (3px wide) on the left of each item. Pure CSS via wp_head().
|
||||||
|
* Items must have the class `av-list-item` (set by av_build_list_section
|
||||||
|
* in _av-bricks-shared.php).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('wp_head', function () {
|
||||||
|
?>
|
||||||
|
<style id="av-list-bullet">
|
||||||
|
/* Each list item gets a vertical gold bar on the left, like a tactical
|
||||||
|
breadcrumb on a UI. Pure CSS, no extra DOM. */
|
||||||
|
/* class order in rendered HTML is "brxe-text-basic av-list-item" — selector
|
||||||
|
works either way (CSS class ordering doesn't matter), kept simple. */
|
||||||
|
.av-list-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 0.9rem;
|
||||||
|
}
|
||||||
|
.av-list-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0.35em; /* aligns with first line baseline */
|
||||||
|
width: 3px;
|
||||||
|
height: 1.1em;
|
||||||
|
background: var(--color-brand-gold, #C9A45A);
|
||||||
|
border-radius: 1.5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}, 999);
|
||||||
40
tools/av-translate-editorial-fr.php
Normal file
40
tools/av-translate-editorial-fr.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Seed FR translations for Editorial pages (one-shot)
|
||||||
|
* /?av_translate_editorial_fr=1
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_translate_editorial_fr'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$fr_titles = [
|
||||||
|
'why-asterion' => 'Pourquoi Asterion',
|
||||||
|
'about' => 'À propos',
|
||||||
|
'customers' => 'Clients',
|
||||||
|
'trust' => 'Confiance & conformité',
|
||||||
|
'partners' => 'Partenaires',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (['why-asterion', 'about', 'customers', 'trust', 'partners'] as $slug) {
|
||||||
|
$en = get_page_by_path($slug);
|
||||||
|
if (! $en) {
|
||||||
|
echo " $slug : EN page not found — run ?av_generate_editorial=1 first\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fr_id = av_translate_page_fr($en->ID, $slug, $fr_titles[$slug]);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo " $slug : EN #{$en->ID} → FR #$fr_id\n";
|
||||||
|
} else {
|
||||||
|
echo " $slug : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Now run /?av_generate_editorial=1 again to push translated structure to FR via auto-sync.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
34
tools/av-translate-forms-fr.php
Normal file
34
tools/av-translate-forms-fr.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Seed FR translations for forms pages (one-shot)
|
||||||
|
* /?av_translate_forms_fr=1
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_translate_forms_fr'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$fr_titles = [
|
||||||
|
'request-demo' => 'Réserver une démo',
|
||||||
|
'request-te-kit' => 'Demander un kit T&E',
|
||||||
|
'request-quote' => 'Demander un devis',
|
||||||
|
'contact' => 'Contact',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (['request-demo', 'request-te-kit', 'request-quote', 'contact'] as $slug) {
|
||||||
|
$en = get_page_by_path($slug);
|
||||||
|
if (! $en) { echo " $slug : EN not found — skip\n"; continue; }
|
||||||
|
$fr_id = av_translate_page_fr($en->ID, $slug, $fr_titles[$slug]);
|
||||||
|
echo $fr_id
|
||||||
|
? " $slug : EN #{$en->ID} → FR #$fr_id\n"
|
||||||
|
: " $slug : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Run /?av_generate_forms=1 to push translated structure.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
40
tools/av-translate-home-fr.php
Normal file
40
tools/av-translate-home-fr.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Seed FR translation for Home page (one-shot)
|
||||||
|
* /?av_translate_home_fr=1
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_translate_home_fr'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$en = get_page_by_path('home');
|
||||||
|
if (! $en) {
|
||||||
|
echo "FAIL : home not found. Run ?av_generate_home=1 first.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fr_id = av_translate_page_fr($en->ID, 'home', 'Accueil');
|
||||||
|
echo $fr_id
|
||||||
|
? "Home EN #{$en->ID} → FR #$fr_id\n"
|
||||||
|
: "Home FR creation FAILED\n";
|
||||||
|
|
||||||
|
// Also set FR home as static front page for FR language via WPML
|
||||||
|
if ($fr_id) {
|
||||||
|
global $sitepress;
|
||||||
|
if ($sitepress && method_exists($sitepress, 'set_setting')) {
|
||||||
|
$settings = (array) $sitepress->get_setting('language_negotiation_type');
|
||||||
|
// WPML language-specific home pages are stored as 'translated-flags' or
|
||||||
|
// similar. The simplest approach is to register the FR home through
|
||||||
|
// WPML's icl_object_id mapping — already done via set_element_language_details.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nNow run /?av_generate_home=1 to push translated structure.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
61
tools/av-translate-industries-fr.php
Normal file
61
tools/av-translate-industries-fr.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Seed FR translations for Industries (one-shot)
|
||||||
|
* Description: /?av_translate_industries_fr=1 creates the FR linked posts for
|
||||||
|
* the Industries hub + 4 detail pages. After that, run av_generate_industries=1
|
||||||
|
* again to push the translated structure via the auto-sync pipeline.
|
||||||
|
*
|
||||||
|
* Order matters : run /?av_generate_industries=1 FIRST (creates EN pages),
|
||||||
|
* then this script (creates linked FR posts), then /?av_generate_industries=1
|
||||||
|
* AGAIN (auto-sync fills FR Bricks content via the shared dictionary).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_translate_industries_fr'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$hub = get_page_by_path('industries');
|
||||||
|
if (! $hub) {
|
||||||
|
echo "FAIL : /industries/ hub not found. Run ?av_generate_industries=1 first.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fr_titles = [
|
||||||
|
'industries' => 'Secteurs',
|
||||||
|
'police' => 'Police',
|
||||||
|
'special-forces' => 'Forces spéciales',
|
||||||
|
'military' => 'Armée',
|
||||||
|
'firefighters' => 'Sapeurs-pompiers',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1) Hub FR
|
||||||
|
$fr_id = av_translate_page_fr($hub->ID, 'industries', $fr_titles['industries']);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo "HUB EN #{$hub->ID} → FR #$fr_id (slug: industries)\n";
|
||||||
|
} else {
|
||||||
|
echo "HUB EN #{$hub->ID} : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Detail pages FR
|
||||||
|
foreach (['police', 'special-forces', 'military', 'firefighters'] as $slug) {
|
||||||
|
$en = get_page_by_path("industries/$slug");
|
||||||
|
if (! $en) {
|
||||||
|
echo " $slug : EN page not found — skipping\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fr_id = av_translate_page_fr($en->ID, $slug, $fr_titles[$slug]);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo " $slug : EN #{$en->ID} → FR #$fr_id\n";
|
||||||
|
} else {
|
||||||
|
echo " $slug : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Now run /?av_generate_industries=1 again to push translated structure to FR via auto-sync.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
61
tools/av-translate-solutions-fr.php
Normal file
61
tools/av-translate-solutions-fr.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Seed FR translations for Solutions (one-shot)
|
||||||
|
* Description: /?av_translate_solutions_fr=1 creates the FR linked posts for
|
||||||
|
* the Solutions hub + 4 detail pages. After that, run av_generate_solutions=1
|
||||||
|
* again to push the translated structure via the auto-sync pipeline.
|
||||||
|
*
|
||||||
|
* Order matters : run /?av_generate_solutions=1 FIRST (creates EN pages),
|
||||||
|
* then this script (creates linked FR posts), then /?av_generate_solutions=1
|
||||||
|
* AGAIN (auto-sync fills the FR Bricks content via the shared dictionary).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_translate_solutions_fr'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$hub = get_page_by_path('solutions');
|
||||||
|
if (! $hub) {
|
||||||
|
echo "FAIL : /solutions/ hub not found. Run ?av_generate_solutions=1 first.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fr_titles = [
|
||||||
|
'solutions' => 'Solutions',
|
||||||
|
'my-proserve' => 'MY PROSERVE',
|
||||||
|
'proserve-flex' => 'PROSERVE FLEX',
|
||||||
|
'proserve-academy' => 'PROSERVE ACADEMY',
|
||||||
|
'customization' => 'PROSERVE+',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1) Hub FR
|
||||||
|
$fr_id = av_translate_page_fr($hub->ID, 'solutions', $fr_titles['solutions']);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo "HUB EN #{$hub->ID} → FR #$fr_id (slug: solutions)\n";
|
||||||
|
} else {
|
||||||
|
echo "HUB EN #{$hub->ID} : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Detail pages FR
|
||||||
|
foreach (['my-proserve', 'proserve-flex', 'proserve-academy', 'customization'] as $slug) {
|
||||||
|
$en = get_page_by_path("solutions/$slug");
|
||||||
|
if (! $en) {
|
||||||
|
echo " $slug : EN page not found — skipping\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fr_id = av_translate_page_fr($en->ID, $slug, $fr_titles[$slug]);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo " $slug : EN #{$en->ID} → FR #$fr_id\n";
|
||||||
|
} else {
|
||||||
|
echo " $slug : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Now run /?av_generate_solutions=1 again to push translated structure to FR via auto-sync.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
59
tools/av-translate-technology-fr.php
Normal file
59
tools/av-translate-technology-fr.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Seed FR translations for Technology (one-shot)
|
||||||
|
* Description: /?av_translate_technology_fr=1 creates the FR linked posts for
|
||||||
|
* the Technology hub + 6 detail pages. Run after ?av_generate_technology=1,
|
||||||
|
* then re-run ?av_generate_technology=1 to push translated structure.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_translate_technology_fr'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$hub = get_page_by_path('technology');
|
||||||
|
if (! $hub) {
|
||||||
|
echo "FAIL : /technology/ hub not found. Run ?av_generate_technology=1 first.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fr_titles = [
|
||||||
|
'technology' => 'Technologie',
|
||||||
|
'vr-hardware' => 'Matériel VR',
|
||||||
|
'software-ai' => 'Logiciel & IA',
|
||||||
|
'weapons-tracking' => 'Armes & Tracking',
|
||||||
|
'scenarios' => 'Scénarios',
|
||||||
|
'instructor-cockpit' => 'Cockpit instructeur',
|
||||||
|
'after-action-review' => 'Debriefing (AAR)',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1) Hub FR
|
||||||
|
$fr_id = av_translate_page_fr($hub->ID, 'technology', $fr_titles['technology']);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo "HUB EN #{$hub->ID} → FR #$fr_id (slug: technology)\n";
|
||||||
|
} else {
|
||||||
|
echo "HUB EN #{$hub->ID} : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Detail pages FR
|
||||||
|
foreach (['vr-hardware', 'software-ai', 'weapons-tracking', 'scenarios', 'instructor-cockpit', 'after-action-review'] as $slug) {
|
||||||
|
$en = get_page_by_path("technology/$slug");
|
||||||
|
if (! $en) {
|
||||||
|
echo " $slug : EN page not found — skipping\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fr_id = av_translate_page_fr($en->ID, $slug, $fr_titles[$slug]);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo " $slug : EN #{$en->ID} → FR #$fr_id\n";
|
||||||
|
} else {
|
||||||
|
echo " $slug : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Now run /?av_generate_technology=1 again to push translated structure to FR via auto-sync.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
185
tools/av-wpml-create-jobs.php
Normal file
185
tools/av-wpml-create-jobs.php
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Create ATE-flagged translate_job rows for our FR pages
|
||||||
|
* Description:
|
||||||
|
* /?av_wpml_create_jobs=1 → for each FR linked page, insert a row in
|
||||||
|
* wp_icl_translate_job with editor='ate' + translated=1, and adjust the
|
||||||
|
* translation_status row to mirror Bricks #14's pattern (status=2 IN_PROGRESS
|
||||||
|
* with uuid + batch_id). This should switch the page-list icon from pencil
|
||||||
|
* ✏️ to gear ⚙️ and route the click to ATE instead of WP editor.
|
||||||
|
*
|
||||||
|
* /?av_wpml_remove_jobs=1 → revert if it breaks something (delete jobs, set
|
||||||
|
* status=10 again).
|
||||||
|
*
|
||||||
|
* This is exploratory : we mimic the structure WPML's internal send_jobs()
|
||||||
|
* creates, without going through TranslationProxy (cloud).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (! empty($_GET['av_wpml_create_jobs'])) av_wpml_create_jobs();
|
||||||
|
if (! empty($_GET['av_wpml_remove_jobs'])) av_wpml_remove_jobs();
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_target_fr_translations() {
|
||||||
|
global $wpdb;
|
||||||
|
$slugs = ['industries', 'police', 'special-forces', 'military', 'firefighters',
|
||||||
|
'solutions', 'my-proserve', 'proserve-flex', 'proserve-academy', 'customization'];
|
||||||
|
$placeholders = implode(',', array_fill(0, count($slugs), '%s'));
|
||||||
|
return $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT p.ID AS post_id, p.post_title AS title, t.trid, t.translation_id, ts.rid, ts.status
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page' AND t.language_code='fr'
|
||||||
|
LEFT JOIN {$wpdb->prefix}icl_translation_status ts ON ts.translation_id = t.translation_id
|
||||||
|
WHERE p.post_name IN ($placeholders)",
|
||||||
|
...$slugs
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_create_jobs() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$rows = av_target_fr_translations();
|
||||||
|
if (! $rows) { echo "No FR translations found\n"; exit; }
|
||||||
|
|
||||||
|
$batch_id = 1; // reuse Bricks #14's batch (or create new one — using 1 for simplicity)
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
if (! $r->rid) {
|
||||||
|
echo "FR #{$r->post_id} '{$r->title}' : no translation_status row, skipping\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Set translation_status to status=2 (ICL_TM_IN_PROGRESS) to match
|
||||||
|
// Bricks #14's pattern. This is what makes the FR column icon = gear ⚙️
|
||||||
|
// and routes the click to the translation editor (CTE).
|
||||||
|
// Note: editing the EN page directly will show a "Translation in progress,
|
||||||
|
// wait before editing" warning dialog — informative only, click "I understand"
|
||||||
|
// to dismiss.
|
||||||
|
$uuid = wp_generate_uuid4();
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'icl_translation_status',
|
||||||
|
[
|
||||||
|
'status' => 2, // ICL_TM_IN_PROGRESS (= gear icon)
|
||||||
|
'needs_update' => 0,
|
||||||
|
'translator_id' => 1,
|
||||||
|
'translation_service' => 'local',
|
||||||
|
'batch_id' => $batch_id,
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'tp_revision' => 1,
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'links_fixed' => 1,
|
||||||
|
],
|
||||||
|
['rid' => $r->rid]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Insert/update translate_job row with editor='ate'
|
||||||
|
$existing_job = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE rid=%d ORDER BY job_id DESC LIMIT 1",
|
||||||
|
$r->rid
|
||||||
|
));
|
||||||
|
|
||||||
|
// editor='wpml' = CTE (Classic Translation Editor), works locally without
|
||||||
|
// cloud sync. translated=1 + completed_date set = the FR is "ready",
|
||||||
|
// matches Bricks #14's pattern (which shows gear icon and opens CTE).
|
||||||
|
// CRITICAL : revision MUST be NULL. WPML's filter_status_css_class() in
|
||||||
|
// class-wpml-tm-translation-status-display.php joins icl_translate_job
|
||||||
|
// with the explicit condition `translate_job.revision IS NULL`. Jobs
|
||||||
|
// with non-null revision are silently ignored → pencil icon, no CTE.
|
||||||
|
$job_data = [
|
||||||
|
'rid' => $r->rid,
|
||||||
|
'translator_id' => 1,
|
||||||
|
'translated' => 1, // FR considered done (we pre-filled via dict)
|
||||||
|
'manager_id' => 1,
|
||||||
|
// 'revision' deliberately omitted → DB default = NULL
|
||||||
|
'title' => $r->title,
|
||||||
|
'deadline_date' => null,
|
||||||
|
'completed_date' => current_time('mysql'),
|
||||||
|
'editor' => 'wpml', // CTE (matches doc_translation_method=1)
|
||||||
|
'editor_job_id' => null,
|
||||||
|
'automatic' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($existing_job) {
|
||||||
|
$wpdb->update($wpdb->prefix . 'icl_translate_job', $job_data, ['job_id' => $existing_job]);
|
||||||
|
// $wpdb->update can't write SQL NULL (it coerces to empty string).
|
||||||
|
// Force revision NULL via raw query for the icon's JOIN to match.
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"UPDATE {$wpdb->prefix}icl_translate_job SET revision = NULL WHERE job_id = %d",
|
||||||
|
$existing_job
|
||||||
|
));
|
||||||
|
echo "FR #{$r->post_id} '{$r->title}' (rid={$r->rid}) : job UPDATED #{$existing_job} (editor={$job_data['editor']}, revision=NULL)\n";
|
||||||
|
} else {
|
||||||
|
$wpdb->insert($wpdb->prefix . 'icl_translate_job', $job_data);
|
||||||
|
$new_job_id = $wpdb->insert_id;
|
||||||
|
// Belt-and-suspenders : ensure revision is NULL even on fresh insert.
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"UPDATE {$wpdb->prefix}icl_translate_job SET revision = NULL WHERE job_id = %d",
|
||||||
|
$new_job_id
|
||||||
|
));
|
||||||
|
echo "FR #{$r->post_id} '{$r->title}' (rid={$r->rid}) : job CREATED #{$new_job_id} (editor={$job_data['editor']}, revision=NULL)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Refresh WP Admin → Pages. The FR column icon should switch from pencil ✏️ to gear ⚙️.\n";
|
||||||
|
echo "Click the gear to open ATE for that page.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_remove_jobs() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$rows = av_target_fr_translations();
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
if (! $r->rid) continue;
|
||||||
|
|
||||||
|
// 1. Remove translate_job rows
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translate_job WHERE rid=%d",
|
||||||
|
$r->rid
|
||||||
|
));
|
||||||
|
|
||||||
|
// 2. Reset status row : ALL the cloud-sync artifacts cleared
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'icl_translation_status',
|
||||||
|
[
|
||||||
|
'status' => 10,
|
||||||
|
'needs_update' => 0,
|
||||||
|
'batch_id' => 0,
|
||||||
|
'uuid' => '',
|
||||||
|
'tp_id' => '',
|
||||||
|
'tp_revision' => 0,
|
||||||
|
'ts_status' => null,
|
||||||
|
'review_status' => null,
|
||||||
|
'ate_comm_retry_count' => 0,
|
||||||
|
'_prevstate' => '',
|
||||||
|
'translation_service' => 'local',
|
||||||
|
'translator_id' => 0,
|
||||||
|
],
|
||||||
|
['rid' => $r->rid]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "FR #{$r->post_id} : job removed, status fully reset\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Cleanup orphan translation_batches rows that may trigger cloud-fetch
|
||||||
|
$orphans = $wpdb->query(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translation_batches
|
||||||
|
WHERE batch_name LIKE 'asterion%' OR id NOT IN
|
||||||
|
(SELECT DISTINCT batch_id FROM {$wpdb->prefix}icl_translation_status WHERE batch_id > 0)"
|
||||||
|
);
|
||||||
|
echo "\nOrphan translation_batches rows cleaned : $orphans\n";
|
||||||
|
|
||||||
|
// 4. Cleanup orphan translation_downloads rows (background ATE sync queue)
|
||||||
|
$tbl_downloads = $wpdb->prefix . 'icl_translation_downloads';
|
||||||
|
if ($wpdb->get_var("SHOW TABLES LIKE '$tbl_downloads'")) {
|
||||||
|
$cleared = $wpdb->query("DELETE FROM $tbl_downloads");
|
||||||
|
echo "Cleared $cleared rows from icl_translation_downloads (pending cloud sync queue)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nReverted to clean state. WPML queue + page list should be back to normal.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
59
tools/av-wpml-debug.php
Normal file
59
tools/av-wpml-debug.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — WPML debug helpers
|
||||||
|
* Description: Read-only URL handlers to inspect WPML state. Localhost only.
|
||||||
|
* - /?av_wpml_status=1 → active languages, default language
|
||||||
|
* - /?av_wpml_translations=ID → translations for a given post id
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_status'])) {
|
||||||
|
av_wpml_status();
|
||||||
|
} elseif (! empty($_GET['av_wpml_translations'])) {
|
||||||
|
av_wpml_translations((int) $_GET['av_wpml_translations']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_status() {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$active = apply_filters('wpml_active_languages', null, []);
|
||||||
|
$default = apply_filters('wpml_default_language', null);
|
||||||
|
$current = apply_filters('wpml_current_language', null);
|
||||||
|
$setup = get_option('icl_sitepress_settings');
|
||||||
|
|
||||||
|
echo "WPML active : " . (function_exists('wpml_loaded') ? 'YES' : '?') . "\n";
|
||||||
|
echo "Default lang: $default\n";
|
||||||
|
echo "Current lang: $current\n";
|
||||||
|
echo "\nActive languages:\n";
|
||||||
|
foreach ((array) $active as $lc => $l) {
|
||||||
|
echo " $lc " . ($l['display_name'] ?? '') . " url=" . ($l['url'] ?? '') . "\n";
|
||||||
|
}
|
||||||
|
echo "\nURL strategy: " . ($setup['language_negotiation_type'] ?? '?') . " (1=lang in dir, 2=domain, 3=lang param)\n";
|
||||||
|
echo "Hidden langs: " . implode(',', $setup['hidden_languages'] ?? []) . "\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_translations($post_id) {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$type = get_post_type($post_id);
|
||||||
|
$title = get_the_title($post_id);
|
||||||
|
$element = 'post_' . $type;
|
||||||
|
$trid = apply_filters('wpml_element_trid', null, $post_id, $element);
|
||||||
|
$translations = apply_filters('wpml_get_element_translations', null, $trid, $element);
|
||||||
|
|
||||||
|
echo "Post #$post_id (type=$type) : \"$title\"\n";
|
||||||
|
echo "trid: " . ($trid ?? 'null') . "\n\n";
|
||||||
|
echo "Translations:\n";
|
||||||
|
foreach ((array) $translations as $lc => $t) {
|
||||||
|
$tid = $t->element_id ?? '?';
|
||||||
|
$name = $t->post_title ?? '?';
|
||||||
|
$slug = get_post($tid)->post_name ?? '?';
|
||||||
|
echo " [$lc] id=$tid slug=$slug title=\"$name\"\n";
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
270
tools/av-wpml-fix-status.php
Normal file
270
tools/av-wpml-fix-status.php
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — WPML translation_status diagnose + fix
|
||||||
|
* Description:
|
||||||
|
* /?av_wpml_status_dump=1 → dump state of every linked FR page (icl_translations
|
||||||
|
* + icl_translation_status + Bricks meta + ATE editor pref)
|
||||||
|
* /?av_wpml_status_fix=1 → insert/update icl_translation_status rows so WPML
|
||||||
|
* marks our programmatically-translated pages as
|
||||||
|
* "completed via ATE", clears the orange "needs update"
|
||||||
|
* icon, and lets the pencil → ATE button work.
|
||||||
|
*
|
||||||
|
* Why : our pipeline writes _bricks_page_content_2 directly via $wpdb. That bypasses
|
||||||
|
* WPML's normal "translation completed" hook. WPML sees the linked posts in
|
||||||
|
* icl_translations but icl_translation_status is empty → the WPML UI thinks the
|
||||||
|
* translation is missing/incomplete and routes the pencil button to the WP editor
|
||||||
|
* fallback instead of the Advanced Translation Editor.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_status_dump'])) {
|
||||||
|
av_wpml_status_dump();
|
||||||
|
}
|
||||||
|
if (! empty($_GET['av_wpml_status_fix'])) {
|
||||||
|
av_wpml_status_fix();
|
||||||
|
}
|
||||||
|
if (! empty($_GET['av_wpml_repair'])) {
|
||||||
|
av_wpml_repair(); // full pipeline : relink-trid → cleanup orphans → status_fix
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_target_trids() {
|
||||||
|
global $wpdb;
|
||||||
|
// All trids that have a /fr/ post WHERE EN parent is /industries/, /solutions/
|
||||||
|
// or the hub pages themselves.
|
||||||
|
return $wpdb->get_col(
|
||||||
|
"SELECT DISTINCT t.trid
|
||||||
|
FROM {$wpdb->prefix}icl_translations t
|
||||||
|
JOIN {$wpdb->posts} p ON p.ID = t.element_id
|
||||||
|
WHERE t.element_type='post_page'
|
||||||
|
AND t.element_id > 0
|
||||||
|
AND (p.post_name IN ('industries','solutions','police','special-forces','military','firefighters','my-proserve','proserve-flex','proserve-academy','customization'))"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_status_dump() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
// 1) Sitepress editor preference
|
||||||
|
$opts = get_option('icl_sitepress_settings');
|
||||||
|
$editor_default = $opts['translation-management']['doc_translation_method'] ?? '(unset)';
|
||||||
|
$editor_alt = $opts['translation_method'] ?? '(unset)';
|
||||||
|
$ate_enabled = $opts['translation-management']['ate']['ate_enabled'] ?? '(unset)';
|
||||||
|
echo "WPML editor preference :\n";
|
||||||
|
echo " doc_translation_method = $editor_default\n";
|
||||||
|
echo " translation_method = $editor_alt\n";
|
||||||
|
echo " ate_enabled = $ate_enabled\n";
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// 2) For each target trid, dump rows
|
||||||
|
$trids = av_wpml_target_trids();
|
||||||
|
if (! $trids) { echo "(no target trids found)\n"; exit; }
|
||||||
|
echo "Found " . count($trids) . " target trid(s) : " . implode(', ', $trids) . "\n\n";
|
||||||
|
|
||||||
|
foreach ($trids as $trid) {
|
||||||
|
echo "── trid $trid ──\n";
|
||||||
|
$tr_rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT translation_id, element_id, language_code, source_language_code
|
||||||
|
FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d ORDER BY language_code",
|
||||||
|
$trid
|
||||||
|
));
|
||||||
|
foreach ($tr_rows as $r) {
|
||||||
|
$title = get_the_title($r->element_id);
|
||||||
|
echo " [{$r->language_code}] post #{$r->element_id} '{$title}' (translation_id={$r->translation_id}, src={$r->source_language_code})\n";
|
||||||
|
|
||||||
|
// status row
|
||||||
|
$st = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT status, needs_update, translation_service, translator_id, md5
|
||||||
|
FROM {$wpdb->prefix}icl_translation_status
|
||||||
|
WHERE translation_id=%d",
|
||||||
|
$r->translation_id
|
||||||
|
));
|
||||||
|
if ($st) {
|
||||||
|
echo " status_row : status=$st->status needs_update=$st->needs_update service=$st->translation_service translator=$st->translator_id md5=" . substr($st->md5 ?? '', 0, 8) . "\n";
|
||||||
|
} else {
|
||||||
|
echo " status_row : MISSING (no row in icl_translation_status)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full repair pipeline :
|
||||||
|
* 1. For each known EN slug, find EN's trid.
|
||||||
|
* 2. Find any FR post with the same slug. If it's linked to a DIFFERENT trid,
|
||||||
|
* relink it to EN's trid (this fixes session-3 re-creation cases where
|
||||||
|
* set_element_language_details fell back to a fresh trid because an orphan
|
||||||
|
* empty FR row blocked the canonical (trid, fr, ...) slot).
|
||||||
|
* 3. Delete orphan FR rows (element_id NULL/0/empty) in the canonical trid.
|
||||||
|
* 4. Reset icl_translation_status (status=10, needs_update=0, md5 from EN content).
|
||||||
|
*/
|
||||||
|
function av_wpml_repair() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$slugs = [
|
||||||
|
// Industries
|
||||||
|
'industries', 'police', 'special-forces', 'military', 'firefighters',
|
||||||
|
// Solutions
|
||||||
|
'solutions', 'my-proserve', 'proserve-flex', 'proserve-academy', 'customization',
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "── STEP 1+2 : align FR posts to EN's trid ──\n\n";
|
||||||
|
foreach ($slugs as $slug) {
|
||||||
|
// EN post for slug
|
||||||
|
$en = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT p.ID, t.trid
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page'
|
||||||
|
WHERE p.post_name=%s AND t.language_code='en'
|
||||||
|
LIMIT 1",
|
||||||
|
$slug
|
||||||
|
));
|
||||||
|
if (! $en) { echo " $slug : no EN page found — skip\n"; continue; }
|
||||||
|
|
||||||
|
// FR post for slug (regardless of current trid)
|
||||||
|
$fr = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT p.ID, t.trid, t.translation_id
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page'
|
||||||
|
WHERE p.post_name=%s AND t.language_code='fr'
|
||||||
|
LIMIT 1",
|
||||||
|
$slug
|
||||||
|
));
|
||||||
|
if (! $fr) { echo " $slug : EN #{$en->ID} (trid={$en->trid}) — no FR yet\n"; continue; }
|
||||||
|
|
||||||
|
if ((int) $fr->trid === (int) $en->trid) {
|
||||||
|
echo " $slug : EN #{$en->ID} ↔ FR #{$fr->ID} aligned on trid={$en->trid} ✓\n";
|
||||||
|
} else {
|
||||||
|
// 1) Remove any orphan FR row in EN's canonical trid
|
||||||
|
$orphans = $wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d AND language_code='fr' AND element_type='post_page'
|
||||||
|
AND (element_id IS NULL OR element_id = 0 OR element_id = '')",
|
||||||
|
$en->trid
|
||||||
|
));
|
||||||
|
// 2) Move FR translation_id to EN's trid
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'icl_translations',
|
||||||
|
['trid' => $en->trid, 'source_language_code' => 'en'],
|
||||||
|
['translation_id' => $fr->translation_id],
|
||||||
|
['%d', '%s'],
|
||||||
|
['%d']
|
||||||
|
);
|
||||||
|
// 3) Delete the now-empty old trid row(s) of FR's previous trid (in case anything else was attached there)
|
||||||
|
// Skip — keep it conservative. The old trid row for FR is now moved, not deleted.
|
||||||
|
|
||||||
|
echo " $slug : FR #{$fr->ID} moved from trid={$fr->trid} → {$en->trid} (deleted $orphans orphan(s))\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n── STEP 3 : remove ALL orphan FR rows across known trids ──\n\n";
|
||||||
|
$trids = av_wpml_target_trids();
|
||||||
|
foreach ($trids as $trid) {
|
||||||
|
$deleted = $wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d AND (element_id IS NULL OR element_id = 0 OR element_id = '')",
|
||||||
|
$trid
|
||||||
|
));
|
||||||
|
if ($deleted > 0) echo " trid $trid : deleted $deleted orphan row(s)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n── STEP 4 : write icl_translation_status (status=10 completed) ──\n\n";
|
||||||
|
av_wpml_status_fix(false);
|
||||||
|
echo "\nDONE.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_status_fix($exit_after = true) {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$trids = av_wpml_target_trids();
|
||||||
|
$patched = 0; $inserted = 0;
|
||||||
|
|
||||||
|
foreach ($trids as $trid) {
|
||||||
|
// Get EN and FR translation_id + post_id
|
||||||
|
$en = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT translation_id, element_id FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d AND language_code='en' AND element_id > 0 LIMIT 1",
|
||||||
|
$trid
|
||||||
|
));
|
||||||
|
$fr = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT translation_id, element_id FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d AND language_code='fr' AND element_id > 0 LIMIT 1",
|
||||||
|
$trid
|
||||||
|
));
|
||||||
|
if (! $en || ! $fr) {
|
||||||
|
echo "trid $trid : EN or FR missing — skip\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute md5 of EN _bricks_page_content_2 (what WPML's translation engine would
|
||||||
|
// hash on a normal flow). When EN changes, md5 differs → WPML shows "needs update".
|
||||||
|
$en_content = get_post_meta($en->element_id, '_bricks_page_content_2', true);
|
||||||
|
if (is_array($en_content)) $en_content = wp_json_encode($en_content);
|
||||||
|
$md5 = md5((string) $en_content);
|
||||||
|
|
||||||
|
// Status codes (WPML constants, hardcoded for portability) :
|
||||||
|
// ICL_TM_NOT_TRANSLATED = 0
|
||||||
|
// ICL_TM_WAITING = 1
|
||||||
|
// ICL_TM_NEEDS_UPDATE = 3
|
||||||
|
// ICL_TM_DUPLICATE = 9
|
||||||
|
// ICL_TM_COMPLETE = 10
|
||||||
|
$data = [
|
||||||
|
'translation_id' => $fr->translation_id,
|
||||||
|
'status' => 10, // ICL_TM_COMPLETE
|
||||||
|
'needs_update' => 0,
|
||||||
|
'md5' => $md5,
|
||||||
|
'translation_service' => 'local', // not delegated to a paid service
|
||||||
|
'translator_id' => 0, // local human (us, programmatic)
|
||||||
|
'translated_by_id' => 0,
|
||||||
|
'links_fixed' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
$existing = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT rid FROM {$wpdb->prefix}icl_translation_status WHERE translation_id=%d",
|
||||||
|
$fr->translation_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// Only update fields that exist (drop optional ones to be safe)
|
||||||
|
$update = $data;
|
||||||
|
unset($update['translation_id']); // not changing PK
|
||||||
|
$wpdb->update($wpdb->prefix . 'icl_translation_status', $update, ['rid' => $existing]);
|
||||||
|
echo "trid $trid : FR #{$fr->element_id} → status row UPDATED (rid=$existing, status=10, md5=" . substr($md5, 0, 8) . ")\n";
|
||||||
|
$patched++;
|
||||||
|
} else {
|
||||||
|
// Be careful : the table may not have all columns. Try with full first, fallback.
|
||||||
|
$ok = $wpdb->insert($wpdb->prefix . 'icl_translation_status', $data);
|
||||||
|
if (! $ok) {
|
||||||
|
// Retry minimal
|
||||||
|
$minimal = [
|
||||||
|
'translation_id' => $fr->translation_id,
|
||||||
|
'status' => 10,
|
||||||
|
'needs_update' => 0,
|
||||||
|
'md5' => $md5,
|
||||||
|
];
|
||||||
|
$ok = $wpdb->insert($wpdb->prefix . 'icl_translation_status', $minimal);
|
||||||
|
}
|
||||||
|
if ($ok) {
|
||||||
|
echo "trid $trid : FR #{$fr->element_id} → status row INSERTED (status=10, md5=" . substr($md5, 0, 8) . ")\n";
|
||||||
|
$inserted++;
|
||||||
|
} else {
|
||||||
|
echo "trid $trid : FR #{$fr->element_id} → INSERT FAILED : " . $wpdb->last_error . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. patched=$patched, inserted=$inserted\n";
|
||||||
|
echo "Refresh WP Admin → Pages — orange refresh icons should clear, pencils should\n";
|
||||||
|
echo "route to WPML's editor (ATE if ate_enabled=1, classic editor otherwise).\n";
|
||||||
|
if ($exit_after) exit;
|
||||||
|
}
|
||||||
50
tools/av-wpml-plugins-check.php
Normal file
50
tools/av-wpml-plugins-check.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Inspect WPML installation state
|
||||||
|
* /?av_wpml_check=1 → liste les plugins WPML installés/actifs + clés de config pertinentes.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (empty($_GET['av_wpml_check'])) return;
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
// Active plugins
|
||||||
|
$active = get_option('active_plugins', []);
|
||||||
|
$all_plugins = get_plugins();
|
||||||
|
echo "WPML-related plugins :\n";
|
||||||
|
foreach ($all_plugins as $file => $data) {
|
||||||
|
if (stripos($file, 'wpml') === false && stripos($file, 'sitepress') === false) continue;
|
||||||
|
$is_active = in_array($file, $active, true);
|
||||||
|
echo sprintf(" %-50s %s (v%s)\n", $file, $is_active ? '[ACTIVE]' : '[inactive]', $data['Version']);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nSitepress settings (extracted) :\n";
|
||||||
|
$opts = get_option('icl_sitepress_settings');
|
||||||
|
$keys = [
|
||||||
|
['translation-management', 'doc_translation_method'],
|
||||||
|
['translation-management', 'ate', 'ate_enabled'],
|
||||||
|
['existing_content_language_verified'],
|
||||||
|
['setup_complete'],
|
||||||
|
['language_negotiation_type'],
|
||||||
|
];
|
||||||
|
foreach ($keys as $path) {
|
||||||
|
$val = $opts;
|
||||||
|
foreach ($path as $k) {
|
||||||
|
$val = is_array($val) && isset($val[$k]) ? $val[$k] : null;
|
||||||
|
if ($val === null) break;
|
||||||
|
}
|
||||||
|
echo " " . implode('.', $path) . " = " . (is_scalar($val) ? var_export($val, true) : '(' . gettype($val) . ')') . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has Translation Management features ?
|
||||||
|
echo "\nFeature detection :\n";
|
||||||
|
echo " class TranslationManagement exists : " . (class_exists('TranslationManagement') ? 'yes' : 'no') . "\n";
|
||||||
|
echo " class WPML_TM_Loader exists : " . (class_exists('WPML_TM_Loader') ? 'yes' : 'no') . "\n";
|
||||||
|
echo " class WPML_String_Translation exists : " . (class_exists('WPML_String_Translation') ? 'yes' : 'no') . "\n";
|
||||||
|
echo " WPML_TM_VERSION defined : " . (defined('WPML_TM_VERSION') ? WPML_TM_VERSION : 'no') . "\n";
|
||||||
|
|
||||||
|
exit;
|
||||||
|
});
|
||||||
122
tools/av-wpml-push-translations.php
Normal file
122
tools/av-wpml-push-translations.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Push dict translations into WPML's icl_strings (so ATE shows them)
|
||||||
|
* Description:
|
||||||
|
* /?av_wpml_strings_dump=1 → dump registered Bricks strings + their FR translation state
|
||||||
|
* /?av_wpml_strings_push=1 → for each EN string in icl_strings (Bricks context), if our
|
||||||
|
* av_fr_translation_dictionary() has a match, write the FR
|
||||||
|
* translation via icl_add_string_translation() with status=10.
|
||||||
|
*
|
||||||
|
* Goal : ATE shows our programmatic translations pre-filled when the linguist opens a page.
|
||||||
|
* Editing in ATE then updates icl_string_translations → WPML rebuilds the FR Bricks JSON.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_strings_dump'])) av_wpml_strings_dump();
|
||||||
|
if (! empty($_GET['av_wpml_strings_push'])) av_wpml_strings_push();
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_strings_dump() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
// 1) Count strings per context
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
"SELECT context, COUNT(*) AS n FROM {$wpdb->prefix}icl_strings GROUP BY context ORDER BY n DESC"
|
||||||
|
);
|
||||||
|
echo "Strings registered (icl_strings) by context :\n";
|
||||||
|
foreach ($rows as $r) echo sprintf(" %-40s %d strings\n", $r->context, $r->n);
|
||||||
|
|
||||||
|
// 1b) Sample strings whose VALUE matches our content (find which context Bricks uses for page content)
|
||||||
|
echo "\nSearching for our content strings :\n";
|
||||||
|
foreach (['PROSERVE', 'Police training', 'Built for those', 'mini-case', 'Industries', 'Solutions'] as $needle) {
|
||||||
|
$found = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT context, name, LEFT(value, 70) AS preview FROM {$wpdb->prefix}icl_strings WHERE value LIKE %s LIMIT 3",
|
||||||
|
'%' . $wpdb->esc_like($needle) . '%'
|
||||||
|
));
|
||||||
|
echo " needle '$needle' : " . count($found) . " match(es)\n";
|
||||||
|
foreach ($found as $f) echo " context='{$f->context}' name='{$f->name}' value='{$f->preview}'\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Sample a few Bricks strings + their FR (context is just 'bricks', not per-page)
|
||||||
|
echo "\nSample Bricks strings (10 of 156) :\n";
|
||||||
|
$strings = $wpdb->get_results(
|
||||||
|
"SELECT id, name, LEFT(value, 80) AS preview, status FROM {$wpdb->prefix}icl_strings WHERE context='bricks' LIMIT 10"
|
||||||
|
);
|
||||||
|
foreach ($strings as $s) {
|
||||||
|
echo " id={$s->id} name={$s->name} status={$s->status}\n EN: {$s->preview}\n";
|
||||||
|
$fr = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT LEFT(value, 80) FROM {$wpdb->prefix}icl_string_translations WHERE string_id=%d AND language='fr' LIMIT 1",
|
||||||
|
$s->id
|
||||||
|
));
|
||||||
|
echo " FR: " . ($fr !== null ? $fr : '(no translation)') . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Count translations by language and status
|
||||||
|
$tr_rows = $wpdb->get_results(
|
||||||
|
"SELECT language, status, COUNT(*) AS n
|
||||||
|
FROM {$wpdb->prefix}icl_string_translations GROUP BY language, status"
|
||||||
|
);
|
||||||
|
echo "\nString translations summary :\n";
|
||||||
|
foreach ($tr_rows as $r) echo " lang={$r->language} status={$r->status} : {$r->n}\n";
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_strings_push() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
if (! function_exists('icl_add_string_translation')) {
|
||||||
|
echo "FAIL : icl_add_string_translation() not available\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the dict to load (with all filter contributions from generators)
|
||||||
|
$dict = av_fr_translation_dictionary();
|
||||||
|
echo "Loaded FR dictionary : " . count($dict) . " EN→FR mappings\n\n";
|
||||||
|
|
||||||
|
// All Bricks page-content strings (per-page contexts : bricks-{POST_ID})
|
||||||
|
$strings = $wpdb->get_results(
|
||||||
|
"SELECT id, value, context FROM {$wpdb->prefix}icl_strings WHERE context LIKE 'bricks-%'"
|
||||||
|
);
|
||||||
|
echo "Found " . count($strings) . " EN strings registered for Bricks pages\n\n";
|
||||||
|
|
||||||
|
$matched = 0;
|
||||||
|
$unmatched = 0;
|
||||||
|
$written = 0;
|
||||||
|
foreach ($strings as $s) {
|
||||||
|
if (! isset($dict[$s->value])) {
|
||||||
|
$unmatched++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$matched++;
|
||||||
|
$fr = $dict[$s->value];
|
||||||
|
// ICL_TM_COMPLETE = 10
|
||||||
|
$result = icl_add_string_translation((int) $s->id, 'fr', $fr, 10);
|
||||||
|
if ($result) $written++;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Matched in dict : $matched\n";
|
||||||
|
echo "Written to icl_string_translations : $written\n";
|
||||||
|
echo "Unmatched (no FR in dict, will stay EN in ATE) : $unmatched\n";
|
||||||
|
|
||||||
|
if ($unmatched > 0 && $unmatched < 100) {
|
||||||
|
echo "\nSample unmatched EN strings (extend the dict to translate these) :\n";
|
||||||
|
$shown = 0;
|
||||||
|
foreach ($strings as $s) {
|
||||||
|
if (! isset($dict[$s->value]) && $shown < 15) {
|
||||||
|
echo " - " . substr($s->value, 0, 120) . (strlen($s->value) > 120 ? '...' : '') . "\n";
|
||||||
|
$shown++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Refresh ATE — FR strings should be pre-filled.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
61
tools/av-wpml-rescan.php
Normal file
61
tools/av-wpml-rescan.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Force WPML to rescan Bricks page strings
|
||||||
|
* Description: Pages created via $wpdb direct write (av-generate-industries.php) bypass
|
||||||
|
* Bricks's save_post hooks, so WPML never gets notified to extract translatable strings
|
||||||
|
* from _bricks_page_content_2. This mu-plugin re-fires the 'wpml_page_builder_register_strings'
|
||||||
|
* action manually so WPML's String Translation registers them.
|
||||||
|
*
|
||||||
|
* Usage :
|
||||||
|
* /?av_wpml_rescan=ID → rescan single post
|
||||||
|
* /?av_wpml_rescan_all=industries → rescan hub + 4 detail pages
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_rescan'])) {
|
||||||
|
$id = (int) $_GET['av_wpml_rescan'];
|
||||||
|
av_wpml_rescan_one($id);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_rescan_all']) && $_GET['av_wpml_rescan_all'] === 'industries') {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
$hub = get_page_by_path('industries');
|
||||||
|
if ($hub) av_wpml_rescan_one($hub->ID, false);
|
||||||
|
foreach (['police','special-forces','military','firefighters'] as $slug) {
|
||||||
|
$p = get_page_by_path("industries/$slug");
|
||||||
|
if ($p) av_wpml_rescan_one($p->ID, false);
|
||||||
|
}
|
||||||
|
echo "Done.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_rescan_one($post_id, $output = true) {
|
||||||
|
$post = get_post($post_id);
|
||||||
|
if (! $post) {
|
||||||
|
if ($output) wp_die("post $post_id not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$package_data = [
|
||||||
|
'kind' => 'Bricks',
|
||||||
|
'name' => (string) $post->ID,
|
||||||
|
'post_id' => $post->ID,
|
||||||
|
'title' => "Bricks (ID {$post->ID})",
|
||||||
|
];
|
||||||
|
|
||||||
|
do_action('wpml_page_builder_register_strings', $post, $package_data);
|
||||||
|
|
||||||
|
if ($output) {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo "Rescan triggered : post #$post_id (\"{$post->post_title}\")\n";
|
||||||
|
echo "WPML String Translation should now have the Bricks strings registered.\n";
|
||||||
|
echo "Return to ATE and reopen the translation — strings should appear.\n";
|
||||||
|
} else {
|
||||||
|
echo " rescanned #$post_id ({$post->post_title})\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
tools/av-wpml-set-cte.php
Normal file
44
tools/av-wpml-set-cte.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Switch WPML translation editor to Classic (CTE)
|
||||||
|
* /?av_wpml_set_cte=1 → updates icl_sitepress_settings.translation-management.doc_translation_method
|
||||||
|
* from 'ATE' to 1 (ICL_TM_TMETHOD_EDITOR = Classic Translation Editor).
|
||||||
|
*
|
||||||
|
* Idempotent. Reverse with ?av_wpml_set_ate=1.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_set_cte'])) av_wpml_set_editor(1); // ICL_TM_TMETHOD_EDITOR
|
||||||
|
if (! empty($_GET['av_wpml_set_ate'])) av_wpml_set_editor('ATE'); // ICL_TM_TMETHOD_ATE
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_set_editor($method) {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$opts = get_option('icl_sitepress_settings');
|
||||||
|
if (! is_array($opts)) $opts = [];
|
||||||
|
if (! isset($opts['translation-management'])) $opts['translation-management'] = [];
|
||||||
|
|
||||||
|
$old = $opts['translation-management']['doc_translation_method'] ?? '(unset)';
|
||||||
|
$opts['translation-management']['doc_translation_method'] = $method;
|
||||||
|
|
||||||
|
// Also clear ate_enabled if switching away from ATE — keeps state coherent
|
||||||
|
if ($method !== 'ATE') {
|
||||||
|
if (isset($opts['translation-management']['ate'])) {
|
||||||
|
$opts['translation-management']['ate']['ate_enabled'] = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isset($opts['translation-management']['ate'])) {
|
||||||
|
$opts['translation-management']['ate']['ate_enabled'] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option('icl_sitepress_settings', $opts);
|
||||||
|
|
||||||
|
echo "WPML doc_translation_method : '" . var_export($old, true) . "' → " . var_export($method, true) . "\n";
|
||||||
|
echo "Refresh WPML → Settings → Translation Editor to verify the new selection.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
125
tools/fresh-setup-mu-plugin.php
Normal file
125
tools/fresh-setup-mu-plugin.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion Fresh Setup (one-shot)
|
||||||
|
* Description: Configures a brand-new LocalWP install for the Asterion VR project — sets siteurl/home, permalinks, activates Bricks child theme + WPML, configures Bricks postTypes. Self-deletes after success.
|
||||||
|
* Version: 0.1.0
|
||||||
|
*
|
||||||
|
* USAGE
|
||||||
|
* -----
|
||||||
|
* After deleting the old LocalWP site and creating a new "Asterion 2026":
|
||||||
|
* 1. Note the new site's HTTP port from LocalWP (likely 10004 again).
|
||||||
|
* 2. Re-create the Junction so the child theme lives in the LocalWP install:
|
||||||
|
* (run as admin from PowerShell)
|
||||||
|
* New-Item -ItemType Junction `
|
||||||
|
* -Path "<NEW_SITE_PATH>\app\public\wp-content\themes\asterion-bricks" `
|
||||||
|
* -Target "C:\ASTERION\GIT\WP2026\wp-content\themes\asterion-bricks"
|
||||||
|
* 3. Extract ThirdParty/bricks.X.Y.Z.zip into the LocalWP themes/ folder.
|
||||||
|
* 4. Extract ThirdParty/sitepress-multilingual-cms.X.Y.Z.zip into plugins/.
|
||||||
|
* 5. If the new port is NOT 10004, edit the AV_TARGET_HOME constant below.
|
||||||
|
* 6. Copy this file to <NEW_SITE_PATH>\app\public\wp-content\mu-plugins\.
|
||||||
|
* 7. Visit http://localhost:<port>/ once — the plugin runs and self-deletes.
|
||||||
|
*
|
||||||
|
* What it sets up
|
||||||
|
* ---------------
|
||||||
|
* - WP_HOME / WP_SITEURL options pointing at localhost:<port>
|
||||||
|
* - Permalinks /%postname%/
|
||||||
|
* - Activates Bricks parent + Asterion Bricks child theme (bricks-bricks-child)
|
||||||
|
* - Activates WPML core
|
||||||
|
* - Configures Bricks postTypes = ['page', 'post', 'case_study', 'scenario']
|
||||||
|
* - Sets the WP timezone to Europe/Paris and locale to en_US (default)
|
||||||
|
*
|
||||||
|
* What it does NOT do (do these manually in WP admin)
|
||||||
|
* ---------------------------------------------------
|
||||||
|
* - Bricks license activation (Bricks → License → paste your key)
|
||||||
|
* - WPML license + setup wizard (WPML → Setup → EN default + FR + directories)
|
||||||
|
* - Bricks Theme Styles (Bricks → Theme Styles → align with tokens.css)
|
||||||
|
* - Build of the Header / Footer / archive Bricks Templates
|
||||||
|
*
|
||||||
|
* @package AsterionBricks
|
||||||
|
* @since 0.2.0
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
// Hardcode the target site URL. Adjust if LocalWP allocated a different port.
|
||||||
|
define('AV_TARGET_HOME', 'http://localhost:10004');
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
|
||||||
|
if (get_option('av_fresh_setup_done') === '1') {
|
||||||
|
$self = __FILE__;
|
||||||
|
add_action('shutdown', function () use ($self) {
|
||||||
|
if (file_exists($self)) {
|
||||||
|
@unlink($self);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log = [];
|
||||||
|
|
||||||
|
// 1. Site URL + home (avoids the localhost:port → / 301 cache trap)
|
||||||
|
update_option('siteurl', AV_TARGET_HOME);
|
||||||
|
update_option('home', AV_TARGET_HOME);
|
||||||
|
$log[] = "Set siteurl/home to " . AV_TARGET_HOME;
|
||||||
|
|
||||||
|
// 2. Pretty permalinks (required for /customers/ /scenarios/ slugs and WPML)
|
||||||
|
if (get_option('permalink_structure') !== '/%postname%/') {
|
||||||
|
update_option('permalink_structure', '/%postname%/');
|
||||||
|
$log[] = "Set permalink_structure to /%postname%/";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Timezone + WP locale defaults
|
||||||
|
update_option('timezone_string', 'Europe/Paris');
|
||||||
|
if (get_locale() !== 'en_US') {
|
||||||
|
update_option('WPLANG', 'en_US');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Activate Bricks child theme (its parent Bricks must be present)
|
||||||
|
$themes = wp_get_themes();
|
||||||
|
if (isset($themes['bricks']) && isset($themes['asterion-bricks'])) {
|
||||||
|
if (get_option('stylesheet') !== 'asterion-bricks') {
|
||||||
|
switch_theme('asterion-bricks');
|
||||||
|
$log[] = "Switched theme to asterion-bricks (parent: bricks)";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$missing = [];
|
||||||
|
if (! isset($themes['bricks'])) $missing[] = 'bricks (parent)';
|
||||||
|
if (! isset($themes['asterion-bricks'])) $missing[] = 'asterion-bricks (child junction)';
|
||||||
|
$log[] = "Theme NOT switched. Missing: " . implode(', ', $missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Activate WPML core
|
||||||
|
if (! function_exists('is_plugin_active')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
$wpml_main = 'sitepress-multilingual-cms/sitepress.php';
|
||||||
|
if (file_exists(WP_PLUGIN_DIR . '/' . $wpml_main)) {
|
||||||
|
if (! is_plugin_active($wpml_main)) {
|
||||||
|
$result = activate_plugin($wpml_main, '', false, true);
|
||||||
|
$log[] = is_wp_error($result)
|
||||||
|
? 'WPML activation FAILED: ' . $result->get_error_message()
|
||||||
|
: 'WPML activated';
|
||||||
|
} else {
|
||||||
|
$log[] = 'WPML already active';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$log[] = "WPML not found at plugins/{$wpml_main}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Bricks: enable Page/Post/CPT in Builder access > postTypes
|
||||||
|
if (defined('BRICKS_DB_GLOBAL_SETTINGS')) {
|
||||||
|
$settings = get_option(BRICKS_DB_GLOBAL_SETTINGS, []);
|
||||||
|
$settings['postTypes'] = ['page', 'post', 'case_study', 'scenario'];
|
||||||
|
update_option(BRICKS_DB_GLOBAL_SETTINGS, $settings);
|
||||||
|
$log[] = "Bricks postTypes set to page/post/case_study/scenario";
|
||||||
|
} else {
|
||||||
|
$log[] = "Bricks not loaded yet — re-run by visiting the site once Bricks is active";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Flush rewrite rules so the new permalink structure takes effect
|
||||||
|
flush_rewrite_rules();
|
||||||
|
|
||||||
|
update_option('av_fresh_setup_done', '1', false);
|
||||||
|
update_option('av_fresh_setup_log', $log, false);
|
||||||
|
|
||||||
|
}, 50);
|
||||||
@@ -1,53 +1,84 @@
|
|||||||
# Asterion Bricks — Child theme
|
# Asterion Bricks — Child theme (minimalist, Bricks-first)
|
||||||
|
|
||||||
Custom WordPress child theme for **asterionvr.com**, built on top of [Bricks Builder](https://bricksbuilder.io). Tactical-cinematic design system for the PROSERVE XR training platform.
|
Custom WordPress child theme for **asterionvr.com**, built on top of [Bricks Builder](https://bricksbuilder.io). The whole site is rendered by Bricks Templates and Bricks-edited pages — this theme exists only to give Bricks a design system, GDPR-compliant fonts, and the project's Custom Post Types.
|
||||||
|
|
||||||
## Structure
|
## What this theme does
|
||||||
|
|
||||||
|
- 🎨 Loads `assets/css/tokens.css` on the public frontend AND inside the Bricks builder iframe — so every `--color-*`, `--space-*`, `--font-*` variable defined there is available to Bricks elements via the **CSS Class** field or **Custom CSS** panel.
|
||||||
|
- 🔤 Self-hosts Inter Variable (latin subset) for RGPD compliance, with a `<link rel="preload">` injected on every page.
|
||||||
|
- 📰 Registers two Custom Post Types — `case_study` (slug `/customers/`) and `scenario` (slug `/scenarios/`) — plus the shared `industry` taxonomy.
|
||||||
|
- 📐 Adds three image sizes : `av-hero` (2400×1350), `av-card` (800×600), `av-thumb` (400×300).
|
||||||
|
- 🌐 Ships an `asterion-bricks` text domain for future i18n.
|
||||||
|
|
||||||
|
## What this theme does NOT do (intentionally)
|
||||||
|
|
||||||
|
- ❌ No `header.php` / `footer.php` / `page.php` / `front-page.php` / `index.php`. Bricks Templates (Header / Footer / Single — Page) drive every screen.
|
||||||
|
- ❌ No Custom Page Templates (`templates/page-*.php`). Pages render through their Bricks data (`_bricks_page_content_2` post meta).
|
||||||
|
- ❌ No PHP renderers, Gutenberg block helpers, shortcodes. Content lives in Bricks data and WPML translations.
|
||||||
|
|
||||||
|
If you find yourself adding any of the above back, stop and revisit `RESTART-PLAN.md` at the repo root — there are usually better Bricks-native alternatives.
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
```
|
```
|
||||||
asterion-bricks/
|
asterion-bricks/
|
||||||
├── style.css # WP theme metadata only (CSS lives in assets/css/)
|
├── style.css # WP theme metadata only
|
||||||
├── functions.php # enqueues, theme support, includes
|
├── functions.php # enqueue tokens, preload font, theme support, CPT include
|
||||||
├── theme.json # Gutenberg color/typo palette
|
├── theme.json # Gutenberg color/typo palette aligned with tokens.css
|
||||||
├── assets/
|
├── README.md # this file
|
||||||
│ ├── css/
|
├── inc/
|
||||||
│ │ ├── tokens.css # design tokens — source of truth
|
│ └── cpt.php # case_study, scenario, industry taxonomy
|
||||||
│ │ ├── utilities.css # CSS reset + utility classes (.av-container, etc.)
|
└── assets/
|
||||||
│ │ └── components.css # component styles (buttons, cards, header, footer…)
|
├── css/
|
||||||
│ ├── js/
|
│ ├── tokens.css # design system source of truth (single file, big)
|
||||||
│ │ └── main.js # vanilla JS, defer-loaded
|
│ └── utilities.css # skip-link, sr-only, focus-visible, reduced motion
|
||||||
│ ├── fonts/ # self-hosted Inter (RGPD)
|
├── fonts/
|
||||||
│ └── img/ # static visuals
|
│ ├── Inter-Variable.woff2
|
||||||
├── templates/ # Bricks template JSON exports (versioned)
|
│ └── Inter-Variable-Italic.woff2
|
||||||
└── inc/
|
└── img/
|
||||||
├── cpt.php # custom post types: case_study, scenario
|
|
||||||
└── seo.php # Schema.org JSON-LD overrides
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
Total : ~10 files, < 700 lines of code (with tokens.css being the lion's share).
|
||||||
|
|
||||||
- **CSS prefix** : `av-` (asterion-vr) on every custom selector to avoid collision with Bricks classes.
|
## Where the content copy lives
|
||||||
- **BEM-light** naming : `.av-card`, `.av-card--feature`, `.av-card__icon`, `.av-card.is-loading`.
|
|
||||||
- **Zero hardcoded values** — every color, spacing, radius and duration goes through `tokens.css`.
|
The textual copy from the Strategy & Content PDF (page-by-page EN copy, deliverable D.1 to D.29) lives at the repo root in `/content-source/`. Those are PHP arrays keyed by page slug — they are not loaded at runtime. Their job is twofold:
|
||||||
- **JS** : vanilla, defer-loaded, prefer `IntersectionObserver` over scroll listeners.
|
- **Reference** when building a Bricks page : copy/paste the appropriate strings into the Bricks elements as you build.
|
||||||
- **A11y** : keyboard reachable, `:focus-visible` outline, ARIA where needed, contrast ≥ 4.5:1, `prefers-reduced-motion` respected.
|
- **Source for programmatic generation** when we replicate a Bricks-built archetype across its variants (e.g. the 4 industry detail pages, the 6 technology pillars).
|
||||||
|
|
||||||
## Activation
|
## Activation
|
||||||
|
|
||||||
1. Ensure Bricks parent is installed at `wp-content/themes/bricks/`.
|
1. Install Bricks parent theme (`wp-content/themes/bricks/`) — the ZIP lives in `ThirdParty/`.
|
||||||
2. WP Admin → Appearance → Themes → activate **Asterion Bricks**.
|
2. Install + activate WPML core (also in `ThirdParty/`).
|
||||||
|
3. WP Admin → Appearance → Themes → activate **Asterion Bricks**.
|
||||||
|
4. WP Admin → Bricks → Settings → check **Page** in « Builder access · Post types ».
|
||||||
|
|
||||||
## Performance budget (per BRIEF section 9)
|
## Tokens to know
|
||||||
|
|
||||||
| Asset | Budget |
|
The full design system lives in `assets/css/tokens.css`. The most-referenced tokens:
|
||||||
|-----------------|-------------------------------|
|
|
||||||
| HTML initial | < 100 KB gzipped |
|
| Variable | Value |
|
||||||
| CSS total | < 50 KB gzipped |
|
|---|---|
|
||||||
| JS initial | < 200 KB gzipped |
|
| `--color-brand-navy` | `#0B1F3A` |
|
||||||
| Hero video | < 2 MB |
|
| `--color-brand-gold` | `#C9A45A` |
|
||||||
| Hero poster | < 150 KB |
|
| `--color-text-primary` | `#0B1F3A` |
|
||||||
| LCP / CLS / TBT | < 2.5s / < 0.1 / < 200ms |
|
| `--font-display` | Inter Display fallback to Inter |
|
||||||
|
| `--font-body` | Inter |
|
||||||
|
| `--space-1..32` | 8-pt grid (4px → 128px) |
|
||||||
|
| `--container-max` | 1280px |
|
||||||
|
| `--ease-default` | `cubic-bezier(0.16, 1, 0.3, 1)` |
|
||||||
|
| `--shadow-sm`/`--shadow-md`/`--shadow-lg` | navy-tinted elevation |
|
||||||
|
|
||||||
|
Reference them inside Bricks via Custom CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
%root% {
|
||||||
|
background: var(--color-brand-navy);
|
||||||
|
color: var(--color-text-on-dark);
|
||||||
|
padding-block: var(--space-20);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL v2 or later. Same as WordPress core.
|
GPL v2 or later, matching WordPress core.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,110 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Asterion Bricks — Utilities
|
* Asterion Bricks — Utilities (minimalist)
|
||||||
*
|
*
|
||||||
* Modern CSS reset + reusable utility classes.
|
* Loaded after tokens.css. Holds the small set of helper classes Bricks
|
||||||
* Loaded after tokens.css, before components.css.
|
* Builder cannot easily provide on its own (skip link, screen-reader-only
|
||||||
*
|
* text, focus ring). Everything else — section padding, container widths,
|
||||||
* Naming : prefix `av-` on every custom class.
|
* button design, card styles — is set inside Bricks via tokens.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ============================================================================
|
/* === A11y skip link — first focusable element on every page === */
|
||||||
Modern CSS reset (inspired by Andy Bell's modern-css-reset, Josh Comeau)
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
|
||||||
|
|
||||||
* { margin: 0; padding: 0; }
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 100%;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: var(--text-body-md);
|
|
||||||
line-height: var(--leading-body-md);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background-color: var(--color-bg-default);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
img, picture, video, canvas, svg {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, button, textarea, select { font: inherit; color: inherit; }
|
|
||||||
|
|
||||||
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol { list-style: none; }
|
|
||||||
|
|
||||||
table { border-collapse: collapse; border-spacing: 0; }
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Heading hierarchy — opinionated defaults consumed when an element
|
|
||||||
carries no Bricks-specific class. Bricks may override, that's expected.
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-h1);
|
|
||||||
line-height: var(--leading-h1);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
letter-spacing: var(--tracking-snug);
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-h2);
|
|
||||||
line-height: var(--leading-h2);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
letter-spacing: var(--tracking-snug);
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-h3);
|
|
||||||
line-height: var(--leading-h3);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
letter-spacing: var(--tracking-snug);
|
|
||||||
}
|
|
||||||
h4 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-h4);
|
|
||||||
line-height: var(--leading-h4);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
max-width: 72ch; /* prose readability per Design Handoff section 2.2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Focus visible — keyboard accessibility
|
|
||||||
Removes outline only when focus came from the mouse; preserves it for keyboard.
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
:focus { outline: none; }
|
|
||||||
|
|
||||||
:focus-visible {
|
|
||||||
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
||||||
outline-offset: var(--focus-ring-offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Skip link — first focusable element on every page (a11y)
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.av-skip-link {
|
.av-skip-link {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -40px;
|
top: -40px;
|
||||||
@@ -114,6 +17,7 @@ p {
|
|||||||
color: var(--color-text-on-dark);
|
color: var(--color-text-on-dark);
|
||||||
font-size: var(--text-body-sm);
|
font-size: var(--text-body-sm);
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
|
text-decoration: none;
|
||||||
z-index: var(--z-toast);
|
z-index: var(--z-toast);
|
||||||
border-bottom-right-radius: var(--radius-md);
|
border-bottom-right-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
@@ -122,10 +26,7 @@ p {
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* === Visually hidden — readable by screen readers, hidden visually === */
|
||||||
Visually hidden — content readable by screen readers, hidden visually
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.av-sr-only {
|
.av-sr-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
@@ -138,77 +39,32 @@ p {
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* === Focus visible — keyboard accessibility on every focusable element === */
|
||||||
Container — centred max-width with responsive padding
|
:focus { outline: none; }
|
||||||
============================================================================ */
|
:focus-visible {
|
||||||
|
outline: var(--focus-ring-width, 2px) solid var(--focus-ring-color, var(--color-info));
|
||||||
.av-container {
|
outline-offset: var(--focus-ring-offset, 2px);
|
||||||
max-width: var(--container-max);
|
|
||||||
margin-inline: auto;
|
|
||||||
padding-inline: var(--container-padding-mobile);
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
/* === Touch target minimum (WCAG AA / Apple HIG) === */
|
||||||
.av-container { padding-inline: var(--container-padding-tablet); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.av-container { padding-inline: var(--container-padding-desktop); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.av-container--narrow {
|
|
||||||
max-width: 800px; /* article body */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Tap target minimum — WCAG AA / Apple HIG (44×44px)
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.av-tap-target {
|
.av-tap-target {
|
||||||
min-width: var(--tap-target-min);
|
min-width: var(--tap-target-min, 44px);
|
||||||
min-height: var(--tap-target-min);
|
min-height: var(--tap-target-min, 44px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* === Reduced motion — respect user preference === */
|
||||||
Stack helpers — vertical rhythm via owl-selector
|
@media (prefers-reduced-motion: reduce) {
|
||||||
============================================================================ */
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
.av-stack > * + * { margin-top: var(--av-stack-gap, var(--space-4)); }
|
animation-iteration-count: 1 !important;
|
||||||
.av-stack--sm > * + * { margin-top: var(--space-2); }
|
transition-duration: 0.01ms !important;
|
||||||
.av-stack--md > * + * { margin-top: var(--space-4); }
|
scroll-behavior: auto !important;
|
||||||
.av-stack--lg > * + * { margin-top: var(--space-8); }
|
}
|
||||||
.av-stack--xl > * + * { margin-top: var(--space-12); }
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Section padding helpers (vertical rhythm between page sections)
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.av-section {
|
|
||||||
padding-block: var(--space-16);
|
|
||||||
}
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.av-section { padding-block: var(--space-20); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.av-section--hero {
|
/*
|
||||||
padding-block: var(--space-24);
|
* Section padding & flex-basis responsive overrides are now handled natively
|
||||||
}
|
* by Bricks via per-breakpoint settings on each element (suffix `:tablet_portrait`,
|
||||||
@media (min-width: 1024px) {
|
* `:mobile_landscape`, `:mobile_portrait` on the setting key). The page generator
|
||||||
.av-section--hero { padding-block: var(--space-32); }
|
* emits those automatically — no custom CSS needed here.
|
||||||
}
|
*/
|
||||||
|
|
||||||
.av-section--dark {
|
|
||||||
background-color: var(--color-bg-dark);
|
|
||||||
color: var(--color-text-on-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Print
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.av-skip-link, .av-no-print { display: none !important; }
|
|
||||||
body { color: #000; background: #fff; }
|
|
||||||
a::after { content: " (" attr(href) ")"; font-size: 0.8em; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asterion Bricks — main JS
|
|
||||||
*
|
|
||||||
* Vanilla JS, defer-loaded. Wraps everything in DOMContentLoaded.
|
|
||||||
* Performance budget : initial bundle < 200 KB gzipped.
|
|
||||||
*
|
|
||||||
* Modules planned :
|
|
||||||
* - Header scroll behavior (background swap after 80px)
|
|
||||||
* - Mega menu trigger (hover desktop, click touch, ESC closes)
|
|
||||||
* - Mobile drawer (full-screen overlay, accordion sections)
|
|
||||||
* - Scroll-triggered fade-in (IntersectionObserver, threshold 0.15)
|
|
||||||
* - Stat callouts count-up (1000ms ease-out)
|
|
||||||
* - Hero video lazy-load + reduced-motion fallback to poster
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect reduced motion once and expose a flag for downstream modules.
|
|
||||||
* Updated reactively if the user toggles the setting mid-session.
|
|
||||||
*/
|
|
||||||
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
||||||
let prefersReducedMotion = reducedMotionQuery.matches;
|
|
||||||
reducedMotionQuery.addEventListener('change', (e) => {
|
|
||||||
prefersReducedMotion = e.matches;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Header scroll behavior.
|
|
||||||
* After 80px scroll, header gets a `is-scrolled` class
|
|
||||||
* (consumed by .av-header.is-scrolled rule in components.css).
|
|
||||||
*/
|
|
||||||
function initHeaderScroll() {
|
|
||||||
const header = document.querySelector('.av-header');
|
|
||||||
if (!header) return;
|
|
||||||
|
|
||||||
let ticking = false;
|
|
||||||
const onScroll = () => {
|
|
||||||
if (!ticking) {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
header.classList.toggle('is-scrolled', window.scrollY > 80);
|
|
||||||
ticking = false;
|
|
||||||
});
|
|
||||||
ticking = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onScroll();
|
|
||||||
window.addEventListener('scroll', onScroll, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll-triggered fade-in (Design Handoff section 5.3).
|
|
||||||
* Sections fade-in + slide-up 16px on entry, threshold 0.15, trigger once.
|
|
||||||
* Skipped entirely if user prefers reduced motion.
|
|
||||||
*/
|
|
||||||
function initScrollReveal() {
|
|
||||||
if (prefersReducedMotion) return;
|
|
||||||
|
|
||||||
const targets = document.querySelectorAll('[data-av-reveal]');
|
|
||||||
if (!targets.length) return;
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('is-revealed');
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { threshold: 0.15, rootMargin: '0px 0px -10% 0px' });
|
|
||||||
|
|
||||||
targets.forEach((target) => observer.observe(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stat count-up (Design Handoff section 5.3).
|
|
||||||
* Element : <span data-av-countup="0|target|duration">target</span>
|
|
||||||
* Animates from 0 to target value over duration ms, ease-out.
|
|
||||||
*/
|
|
||||||
function initStatCountUp() {
|
|
||||||
if (prefersReducedMotion) return;
|
|
||||||
|
|
||||||
const stats = document.querySelectorAll('[data-av-countup]');
|
|
||||||
if (!stats.length) return;
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (!entry.isIntersecting) return;
|
|
||||||
const el = entry.target;
|
|
||||||
const target = parseFloat(el.dataset.avCountup) || 0;
|
|
||||||
const duration = parseInt(el.dataset.avCountupDuration || '1000', 10);
|
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
const tick = (now) => {
|
|
||||||
const elapsed = Math.min((now - start) / duration, 1);
|
|
||||||
// ease-out cubic
|
|
||||||
const eased = 1 - Math.pow(1 - elapsed, 3);
|
|
||||||
el.textContent = Math.round(target * eased).toLocaleString();
|
|
||||||
if (elapsed < 1) requestAnimationFrame(tick);
|
|
||||||
};
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
observer.unobserve(el);
|
|
||||||
});
|
|
||||||
}, { threshold: 0.5 });
|
|
||||||
|
|
||||||
stats.forEach((s) => observer.observe(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mobile drawer toggle.
|
|
||||||
* Trigger : <button class="av-drawer-toggle" aria-controls="av-drawer">
|
|
||||||
* Drawer : <div id="av-drawer" class="av-drawer">
|
|
||||||
*/
|
|
||||||
function initMobileDrawer() {
|
|
||||||
const toggle = document.querySelector('.av-drawer-toggle');
|
|
||||||
const drawer = document.getElementById('av-drawer');
|
|
||||||
if (!toggle || !drawer) return;
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
drawer.classList.add('is-open');
|
|
||||||
toggle.setAttribute('aria-expanded', 'true');
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
};
|
|
||||||
const close = () => {
|
|
||||||
drawer.classList.remove('is-open');
|
|
||||||
toggle.setAttribute('aria-expanded', 'false');
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
toggle.addEventListener('click', () => {
|
|
||||||
drawer.classList.contains('is-open') ? close() : open();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape' && drawer.classList.contains('is-open')) close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boot.
|
|
||||||
*/
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
initHeaderScroll();
|
|
||||||
initScrollReveal();
|
|
||||||
initStatCountUp();
|
|
||||||
initMobileDrawer();
|
|
||||||
});
|
|
||||||
|
|
||||||
})();
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Global footer
|
|
||||||
*
|
|
||||||
* Per Design Handoff section 3.5 :
|
|
||||||
* - 5 columns desktop : Solutions / Industries / Why Asterion / Resources / Newsletter
|
|
||||||
* - Stack on mobile (could become accordions later)
|
|
||||||
* - Bottom strip : copyright · privacy · terms · cookies · social · language switcher
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$av_year = date_i18n('Y');
|
|
||||||
|
|
||||||
// Mirror the bail-out in header.php: don't emit our footer chrome inside
|
|
||||||
// the Bricks builder canvas/iframe.
|
|
||||||
if (function_exists('bricks_is_builder') && bricks_is_builder()) {
|
|
||||||
wp_footer();
|
|
||||||
echo '</body></html>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</main><!-- /#main -->
|
|
||||||
|
|
||||||
<footer class="av-footer" role="contentinfo">
|
|
||||||
<div class="av-container">
|
|
||||||
|
|
||||||
<div class="av-footer__cols">
|
|
||||||
|
|
||||||
<div class="av-footer__brand">
|
|
||||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="av-header__logo" aria-label="Asterion VR — home">
|
|
||||||
ASTERION VR
|
|
||||||
</a>
|
|
||||||
<p>
|
|
||||||
<?php echo esc_html(av_t('The modular, sovereign XR platform for European security forces. Train like the threat is now.', 'asterion-bricks')); ?>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__col">
|
|
||||||
<h2 class="av-footer__col-title"><?php echo esc_html(av_t('Solutions', 'asterion-bricks')); ?></h2>
|
|
||||||
<ul class="av-footer__list">
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/solutions/my-proserve/')); ?>">MY PROSERVE</a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/solutions/proserve-flex/')); ?>">PROSERVE FLEX</a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/solutions/proserve-academy/')); ?>">PROSERVE ACADEMY</a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/solutions/customization/')); ?>">PROSERVE+</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__col">
|
|
||||||
<h2 class="av-footer__col-title"><?php echo esc_html(av_t('Industries', 'asterion-bricks')); ?></h2>
|
|
||||||
<ul class="av-footer__list">
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/industries/police/')); ?>"><?php echo esc_html(av_t('Police', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/industries/special-forces/')); ?>"><?php echo esc_html(av_t('Special Forces', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/industries/military/')); ?>"><?php echo esc_html(av_t('Military', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/industries/firefighters/')); ?>"><?php echo esc_html(av_t('Firefighters', 'asterion-bricks')); ?></a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__col">
|
|
||||||
<h2 class="av-footer__col-title"><?php echo esc_html(av_t('Why Asterion', 'asterion-bricks')); ?></h2>
|
|
||||||
<ul class="av-footer__list">
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/about/')); ?>"><?php echo esc_html(av_t('Our story', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/customers/')); ?>"><?php echo esc_html(av_t('Customers', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/trust/')); ?>"><?php echo esc_html(av_t('Trust & Compliance', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/partners/')); ?>"><?php echo esc_html(av_t('Partners', 'asterion-bricks')); ?></a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__newsletter">
|
|
||||||
<h2 class="av-footer__col-title"><?php echo esc_html(av_t('Newsletter', 'asterion-bricks')); ?></h2>
|
|
||||||
<p style="color:var(--color-text-on-dark-muted);font-size:var(--text-body-sm);">
|
|
||||||
<?php echo esc_html(av_t('Insights from the front of immersive training. One email per month. No spam.', 'asterion-bricks')); ?>
|
|
||||||
</p>
|
|
||||||
<form class="av-footer__newsletter-form" method="post" action="#" aria-label="<?php esc_attr_e('Newsletter signup', 'asterion-bricks'); ?>">
|
|
||||||
<label for="av-newsletter-email" class="av-sr-only"><?php echo esc_html(av_t('Your email', 'asterion-bricks')); ?></label>
|
|
||||||
<input type="email" id="av-newsletter-email" name="email" class="av-input" placeholder="<?php esc_attr_e('your@email.com', 'asterion-bricks'); ?>" required>
|
|
||||||
<button type="submit" class="av-btn av-btn--primary av-btn--md"><?php echo esc_html(av_t('Subscribe', 'asterion-bricks')); ?></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__bottom">
|
|
||||||
<div>© <?php echo esc_html($av_year); ?> Asterion VR. <?php echo esc_html(av_t('All rights reserved.', 'asterion-bricks')); ?></div>
|
|
||||||
<div class="av-footer__bottom-links">
|
|
||||||
<a href="<?php echo esc_url(home_url('/legal/privacy/')); ?>"><?php echo esc_html(av_t('Privacy', 'asterion-bricks')); ?></a>
|
|
||||||
<a href="<?php echo esc_url(home_url('/legal/terms/')); ?>"><?php echo esc_html(av_t('Terms', 'asterion-bricks')); ?></a>
|
|
||||||
<a href="<?php echo esc_url(home_url('/legal/cookies/')); ?>"><?php echo esc_html(av_t('Cookies', 'asterion-bricks')); ?></a>
|
|
||||||
<a href="<?php echo esc_url(home_url('/legal/notice/')); ?>"><?php echo esc_html(av_t('Legal notice', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-footer__social">
|
|
||||||
<a href="https://www.linkedin.com/company/asterion-vr/" rel="noopener" aria-label="LinkedIn">LinkedIn</a>
|
|
||||||
·
|
|
||||||
<a href="https://www.youtube.com/" rel="noopener" aria-label="YouTube">YouTube</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<?php wp_footer(); ?>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Front page (home)
|
|
||||||
*
|
|
||||||
* Implements the 9-section home wireframe from the Design Handoff
|
|
||||||
* (section 4.1) using the EN copy from Strategy & Content (D.1).
|
|
||||||
*
|
|
||||||
* 1. Hero — dark, full-bleed video, signature
|
|
||||||
* 2. Trust bar — sector marks + 4 inline metrics
|
|
||||||
* 3. Solutions — 3 cards (MY / FLEX / ACADEMY)
|
|
||||||
* 4. Industries — 4 cards 2x2 (Police / SF / Military / Firefighters)
|
|
||||||
* 5. Feature heroes — 3 alternating (UWeD / Instructor Cockpit / AAR)
|
|
||||||
* 6. Featured case — single highlighted case
|
|
||||||
* 7. Testimonials — 3 quotes (slider becomes JS-driven later)
|
|
||||||
* 8. Latest insights — 3 blog cards
|
|
||||||
* 9. Conversion banner — Demo / T&E / Quote
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$theme = get_stylesheet_directory_uri();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 1. HERO -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-hero" aria-labelledby="hero-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<?php /* Hero video placeholder. Replace with a 25-35s loop, MP4 H.264 + WebM, < 2 MB, per Design Handoff 5.4. */ ?>
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 20% 30%, rgba(201,164,90,0.15), transparent 60%),
|
|
||||||
radial-gradient(ellipse at 80% 70%, rgba(30,91,168,0.20), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Modular XR training for security forces', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="hero-headline" class="av-hero__headline">Your Last Mistake Should Be in VR.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
PROSERVE™ is the modular, fully portable XR platform built for police, special forces, military and firefighters. Deploy in five minutes. Train anywhere. Debrief everything.
|
|
||||||
</p>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--xl"><?php esc_html_e('Request a Demo', 'asterion-bricks'); ?></a>
|
|
||||||
<a href="#feature-heroes" class="av-btn av-btn--on-dark av-btn--secondary av-btn--xl"><?php esc_html_e('Watch the 90-second film', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 2. TRUST BAR -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-logo-strip" aria-label="<?php esc_attr_e('Trusted by', 'asterion-bricks'); ?>">
|
|
||||||
<div class="av-container">
|
|
||||||
<p class="av-logo-strip__intro"><?php esc_html_e('Trusted by security forces across Europe', 'asterion-bricks'); ?></p>
|
|
||||||
<div class="av-logo-strip__row">
|
|
||||||
<?php /* Real client logos pending NDA confirmation — see MISSING-ASSETS.md. */ ?>
|
|
||||||
<?php for ($i = 1; $i <= 6; $i++) : ?>
|
|
||||||
<div class="av-logo-strip__logo">Logo <?php echo (int) $i; ?></div>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</div>
|
|
||||||
<div class="av-logo-strip__metrics">
|
|
||||||
<div class="av-logo-strip__metric"><?php esc_html_e('10 years of XR research for security forces', 'asterion-bricks'); ?></div>
|
|
||||||
<div class="av-logo-strip__metric"><?php esc_html_e('11 environments · 50+ scenarios · monthly updates', 'asterion-bricks'); ?></div>
|
|
||||||
<div class="av-logo-strip__metric"><?php esc_html_e('< 5 minutes from case to active training', 'asterion-bricks'); ?></div>
|
|
||||||
<div class="av-logo-strip__metric"><?php esc_html_e('Made in France · GDPR-native · works fully offline', 'asterion-bricks'); ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 3. SOLUTIONS -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section av-section--dark" aria-labelledby="solutions-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('Solutions', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="solutions-title" class="av-section__title">Three solutions. One platform. Zero compromise.</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
From the lone officer rebuilding muscle memory to a 16-trainee unit running a coordinated assault — PROSERVE scales with your mission, not the other way around.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-grid-3">
|
|
||||||
|
|
||||||
<article class="av-card is-on-dark">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#13294B,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Solo training', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title">MY PROSERVE</h3>
|
|
||||||
<p class="av-card__excerpt">
|
|
||||||
Mini-case. Anywhere, anytime. Built for the officer who needs to train 30 minutes between two shifts. 9 m² of free space, a headset, a real-recoil replica. The legal minimum becomes the daily standard.
|
|
||||||
</p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/my-proserve/'); ?>" class="av-link av-link--on-dark av-link--with-arrow"><?php esc_html_e('Discover MY PROSERVE', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card is-on-dark">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#13294B,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Team training · 1 to 5', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title">PROSERVE FLEX</h3>
|
|
||||||
<p class="av-card__excerpt">
|
|
||||||
One case per operator. Multi-user cooperative scenarios, instructor PC, full AAR. Deploy on any flat surface from 9 to 100 m². For tactical units that move with their training.
|
|
||||||
</p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/proserve-flex/'); ?>" class="av-link av-link--on-dark av-link--with-arrow"><?php esc_html_e('Discover PROSERVE FLEX', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card is-on-dark">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#13294B,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Unit training · 1 to 16', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title">PROSERVE ACADEMY</h3>
|
|
||||||
<p class="av-card__excerpt">
|
|
||||||
The full training centre, in your facility. Up to 900 m² of free-roam space, instructor cockpit, racked compute, parallel sessions. Built for academies and continuous training programs.
|
|
||||||
</p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/proserve-academy/'); ?>" class="av-link av-link--on-dark av-link--with-arrow"><?php esc_html_e('Discover PROSERVE ACADEMY', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 4. INDUSTRIES -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section" aria-labelledby="industries-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Industries', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="industries-title" class="av-section__title">Built with operators, for operators.</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
PROSERVE is not a generic VR platform repackaged for the public sector. Every scenario, every weapon profile, every instructor metric was specified by serving and former operators in close partnership with our engineering team.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-grid-4" style="grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));">
|
|
||||||
|
|
||||||
<article class="av-card">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('National & Municipal', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php esc_html_e('Police', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-card__excerpt">Vehicle stops, identity checks, urban CQB, de-escalation, riot containment. Built for the realities of contemporary policing.</p>
|
|
||||||
<div class="av-card__cta"><a href="<?php echo esc_url($home_url . 'industries/police/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Police training', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Tactical Units', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php esc_html_e('Special Forces', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-card__excerpt">Hostage rescue, dynamic entries, hostile crowd extraction, sniper-spotter coordination. Built for the units where every error has a name.</p>
|
|
||||||
<div class="av-card__cta"><a href="<?php echo esc_url($home_url . 'industries/special-forces/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Special forces training', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Armed forces', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php esc_html_e('Military', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-card__excerpt">MOUT, convoy escort, FOB defense, IED awareness, joint operation drills. Built for armed forces that deploy fast and adapt faster.</p>
|
|
||||||
<div class="av-card__cta"><a href="<?php echo esc_url($home_url . 'industries/military/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Military training', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Emergency services', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php esc_html_e('Firefighters', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-card__excerpt">Structure fires, hazmat, multi-casualty triage, confined-space rescue, industrial accidents. Built for first responders who can't rehearse the worst day at full scale.</p>
|
|
||||||
<div class="av-card__cta"><a href="<?php echo esc_url($home_url . 'industries/firefighters/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Firefighter training', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 5. FEATURE HEROES (3 alternating) -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section id="feature-heroes" class="av-section av-section--dark" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('Technology', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title">Behavior, not scripts.</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$features = [
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Real-recoil weapon integration',
|
|
||||||
'title' => 'The weapon you train with is the weapon you carry.',
|
|
||||||
'body' => 'Our Universal Weapon Digitizer (UWeD™) attaches to any gas-blowback or CO2 replica — Glock 17, HK416, MP7, M9, and dozens more — or to your real service weapon firing blanks. Bullet trajectory is physically simulated: ricochet, penetration, material density. Recoil-induced tracking loss is corrected by an exclusive IMU compensation system.',
|
|
||||||
'link' => 'See how UWeD works',
|
|
||||||
'href' => '/technology/weapons-tracking/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Instructor Cockpit',
|
|
||||||
'title' => 'You orchestrate. The trainees adapt.',
|
|
||||||
'body' => "From a dedicated instructor PC, you orchestrate every detail of the scenario in real time. Walk through walls. Switch to any trainee's first-person view. Toggle field of view, shot traces, and threat highlighting. Add or remove civilians, change a hostage's behavior, escalate or de-escalate a contact — without pausing the scenario.",
|
|
||||||
'link' => 'Inside the Instructor Cockpit',
|
|
||||||
'href' => '/technology/instructor-cockpit/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'After-Action Review',
|
|
||||||
'title' => 'The session ends; the learning starts.',
|
|
||||||
'body' => "Every event is logged: shots fired, shots hit, shots missed by target type, reaction time, field-of-view direction, weapon muzzling, position over time. Replay any session from any angle — top-down tactical, free-camera, or in-eye view of any trainee. Speed up, slow down, pause. Compare a recruit's reaction time to the unit's median.",
|
|
||||||
'link' => 'Explore After-Action Review',
|
|
||||||
'href' => '/technology/after-action-review/',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
foreach ($features as $i => $f) :
|
|
||||||
$reverse = ($i % 2 === 1);
|
|
||||||
?>
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-8);align-items:center;margin-bottom:var(--space-16);<?php echo $reverse ? 'direction:rtl;' : ''; ?>" class="av-feature-row">
|
|
||||||
<div style="aspect-ratio:16/9;background:linear-gradient(135deg,#13294B,#081427);border-radius:var(--radius-md);direction:ltr;"></div>
|
|
||||||
<div style="direction:ltr;display:flex;flex-direction:column;gap:var(--space-3);">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php echo esc_html($f['eyebrow']); ?></span>
|
|
||||||
<h3 style="font-family:var(--font-display);font-size:var(--text-display-md);line-height:var(--leading-display-md);font-weight:var(--font-weight-bold);letter-spacing:var(--tracking-tight);color:var(--color-text-on-dark);margin:0;"><?php echo esc_html($f['title']); ?></h3>
|
|
||||||
<p style="color:var(--color-text-on-dark-muted);font-size:var(--text-body-lg);line-height:var(--leading-body-lg);max-width:60ch;"><?php echo wp_kses_post($f['body']); ?></p>
|
|
||||||
<div><a href="<?php echo esc_url($home_url . ltrim($f['href'], '/')); ?>" class="av-link av-link--on-dark av-link--with-arrow"><?php echo esc_html($f['link']); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<style>
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.av-feature-row { grid-template-columns: 1fr 1fr !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 6. FEATURED CASE STUDY -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section" aria-labelledby="case-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-10);align-items:center;" class="av-case-row">
|
|
||||||
<div style="aspect-ratio:4/3;background:linear-gradient(135deg,#3E4C5E,#0B1F3A);border-radius:var(--radius-md);"></div>
|
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-4);">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Case study', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="case-title" class="av-section__title">How a European tactical unit cut tactical errors by 35%.</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
In 2025, a European tactical unit deployed PROSERVE FLEX as part of its continuous training program. Six months in, instructor-rated tactical errors dropped 35% across the unit. Reaction time improved 22%. And weekly training volume tripled, without a single live round fired.
|
|
||||||
</p>
|
|
||||||
<div style="display:flex;gap:var(--space-8);margin-top:var(--space-2);">
|
|
||||||
<div class="av-stat av-stat--gold"><div class="av-stat__value">-35%</div><div class="av-stat__label"><?php esc_html_e('Tactical errors', 'asterion-bricks'); ?></div></div>
|
|
||||||
<div class="av-stat av-stat--gold"><div class="av-stat__value">+22%</div><div class="av-stat__label"><?php esc_html_e('Reaction time', 'asterion-bricks'); ?></div></div>
|
|
||||||
<div class="av-stat av-stat--gold"><div class="av-stat__value">×3</div><div class="av-stat__label"><?php esc_html_e('Training volume', 'asterion-bricks'); ?></div></div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:var(--space-2);"><a href="<?php echo esc_url($home_url . 'customers/'); ?>" class="av-btn av-btn--secondary av-btn--md"><?php esc_html_e('Read the case study', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
@media (min-width: 1024px) { .av-case-row { grid-template-columns: 1fr 1fr !important; } }
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 7. TESTIMONIALS -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section" style="background-color:var(--color-bg-subtle);" aria-labelledby="testimonials-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head" style="text-align:center;align-items:center;margin-inline:auto;">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Voices from the field', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="testimonials-title" class="av-section__title" style="text-align:center;margin-inline:auto;">Train how it really feels.</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<article class="av-card av-card--quote">
|
|
||||||
<div class="av-card__quote">We've stopped asking how often we train. We've started measuring how well.</div>
|
|
||||||
<div class="av-card__attribution">— Tactical training officer, European federal police (anonymized)</div>
|
|
||||||
</article>
|
|
||||||
<article class="av-card av-card--quote">
|
|
||||||
<div class="av-card__quote">The first time my recruits saw their own AAR replay, three of them asked to redo the drill. That had never happened before.</div>
|
|
||||||
<div class="av-card__attribution">— Senior instructor, national police academy</div>
|
|
||||||
</article>
|
|
||||||
<article class="av-card av-card--quote">
|
|
||||||
<div class="av-card__quote">What used to take a half-day live exercise now takes 20 minutes between shifts.</div>
|
|
||||||
<div class="av-card__attribution">— Captain, urban response unit</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 8. LATEST INSIGHTS -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section" aria-labelledby="insights-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Insights', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="insights-title" class="av-section__title">Insights from the front of immersive training.</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$insights = get_posts(['numberposts' => 3, 'post_status' => 'publish']);
|
|
||||||
if (! empty($insights)) :
|
|
||||||
?>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($insights as $post) : setup_postdata($post); ?>
|
|
||||||
<article class="av-card av-card--blog">
|
|
||||||
<a href="<?php the_permalink(); ?>" class="av-card__media" aria-hidden="true" tabindex="-1" style="background:linear-gradient(135deg,#CCD3DC,#E5E9EE);"></a>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php echo esc_html(get_the_date('M j, Y')); ?></span>
|
|
||||||
<h3 class="av-card__title"><a href="<?php the_permalink(); ?>" style="color:inherit;text-decoration:none;"><?php the_title(); ?></a></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html(wp_trim_words(get_the_excerpt(), 22)); ?></p>
|
|
||||||
<div class="av-card__cta"><a href="<?php the_permalink(); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Read more', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; wp_reset_postdata(); ?>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<p style="color:var(--color-text-muted);">
|
|
||||||
<?php esc_html_e('Editorial blog launches with the site go-live. The 12-month content calendar is in the Strategy & Content brief, section B.4.', 'asterion-bricks'); ?>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div style="margin-top:var(--space-8);">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'insights/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('See all articles', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 9. CONVERSION BANNER -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-conversion-banner" aria-labelledby="conversion-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-conversion-banner__head">
|
|
||||||
<h2 id="conversion-title" class="av-conversion-banner__title">Train like the threat is now.</h2>
|
|
||||||
<p class="av-conversion-banner__lead"><?php esc_html_e('Three paths into the platform — pick the one that fits where you are.', 'asterion-bricks'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__grid">
|
|
||||||
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a Demo', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('Schedule a 30-minute tailored session with our team. Receive a video demo of the configuration that matches your needs.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--md"><?php esc_html_e('Schedule a demo', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a T&E Kit', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('Get a PROSERVE case on loan at your facility for 5 to 10 days, with on-site support from our team.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-te-kit/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md"><?php esc_html_e('Request the kit', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a Quote', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('Get a detailed quote and deployment plan within 5 working days.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-quote/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md"><?php esc_html_e('Get a quote', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,35 +1,38 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Asterion Bricks — child theme bootstrap
|
* Asterion Bricks — Child theme bootstrap (minimalist, Bricks-first)
|
||||||
*
|
*
|
||||||
* Loads design tokens, utilities, components, and main JS in a deterministic
|
* Goals
|
||||||
* order. Registers theme support, image sizes, and includes modular pieces
|
* -----
|
||||||
* (custom post types, SEO schemas).
|
* 1. Provide the design system to Bricks Builder via tokens.css.
|
||||||
|
* 2. Self-host Inter fonts so Bricks-rendered pages stay GDPR-compliant.
|
||||||
|
* 3. Register Custom Post Types (case_study, scenario) used across the site.
|
||||||
|
*
|
||||||
|
* Non-goals (intentionally NOT here, on purpose)
|
||||||
|
* ----------------------------------------------
|
||||||
|
* ✗ No header.php / footer.php / page.php / front-page.php / index.php.
|
||||||
|
* Bricks Templates (Header / Footer / Single — Page) drive the chrome.
|
||||||
|
* ✗ No Custom Page Templates. Pages render through Bricks data.
|
||||||
|
* ✗ No PHP renderers. Content lives in Bricks data + WPML translations.
|
||||||
|
* ✗ No Gutenberg block libraries dequeued. Bricks needs them in the builder.
|
||||||
*
|
*
|
||||||
* @package AsterionBricks
|
* @package AsterionBricks
|
||||||
* @since 0.1.0
|
* @since 0.2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
define('AV_THEME_VERSION', '0.1.0');
|
define('AV_THEME_VERSION', '0.2.0');
|
||||||
define('AV_THEME_DIR', get_stylesheet_directory());
|
define('AV_THEME_DIR', get_stylesheet_directory());
|
||||||
define('AV_THEME_URI', get_stylesheet_directory_uri());
|
define('AV_THEME_URI', get_stylesheet_directory_uri());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue child theme styles and scripts.
|
* Enqueue tokens.css + utilities.css on the public frontend AND inside the
|
||||||
*
|
* Bricks Builder iframe — the builder loads the front-end stylesheet so we
|
||||||
* Bricks parent stylesheet is loaded by the parent theme itself — no need to
|
* see our `--color-*`, `--space-*`, `--font-*` variables as we work.
|
||||||
* re-enqueue. We layer our tokens → utilities → components on top.
|
|
||||||
*
|
|
||||||
* Cache busting via `filemtime()` so dev changes are picked up without manual
|
|
||||||
* version bumps. In production, swap to AV_THEME_VERSION + a build step.
|
|
||||||
*/
|
*/
|
||||||
add_action('wp_enqueue_scripts', function () {
|
add_action('wp_enqueue_scripts', function () {
|
||||||
$css_dir = AV_THEME_DIR . '/assets/css';
|
$css_dir = AV_THEME_DIR . '/assets/css';
|
||||||
$css_uri = AV_THEME_URI . '/assets/css';
|
$css_uri = AV_THEME_URI . '/assets/css';
|
||||||
$js_dir = AV_THEME_DIR . '/assets/js';
|
|
||||||
$js_uri = AV_THEME_URI . '/assets/js';
|
|
||||||
|
|
||||||
if (file_exists($css_dir . '/tokens.css')) {
|
if (file_exists($css_dir . '/tokens.css')) {
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
@@ -39,7 +42,6 @@ add_action('wp_enqueue_scripts', function () {
|
|||||||
filemtime($css_dir . '/tokens.css')
|
filemtime($css_dir . '/tokens.css')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists($css_dir . '/utilities.css')) {
|
if (file_exists($css_dir . '/utilities.css')) {
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'av-utilities',
|
'av-utilities',
|
||||||
@@ -48,82 +50,25 @@ add_action('wp_enqueue_scripts', function () {
|
|||||||
filemtime($css_dir . '/utilities.css')
|
filemtime($css_dir . '/utilities.css')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists($css_dir . '/components.css')) {
|
|
||||||
wp_enqueue_style(
|
|
||||||
'av-components',
|
|
||||||
$css_uri . '/components.css',
|
|
||||||
['av-tokens', 'av-utilities'],
|
|
||||||
filemtime($css_dir . '/components.css')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists($js_dir . '/main.js')) {
|
|
||||||
wp_enqueue_script(
|
|
||||||
'av-main',
|
|
||||||
$js_uri . '/main.js',
|
|
||||||
[],
|
|
||||||
filemtime($js_dir . '/main.js'),
|
|
||||||
['strategy' => 'defer', 'in_footer' => true]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 20);
|
}, 20);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preload self-hosted Inter fonts to reduce LCP.
|
* Preload the Inter Variable WOFF2 to reduce LCP. Conditional on the file
|
||||||
*
|
* existing so partial deploys don't 404.
|
||||||
* Only emits <link rel="preload"> for fonts that actually exist on disk —
|
|
||||||
* silent during early dev when the WOFF2 files have not been dropped yet.
|
|
||||||
*/
|
*/
|
||||||
add_action('wp_head', function () {
|
add_action('wp_head', function () {
|
||||||
$fonts = [
|
$font_path = AV_THEME_DIR . '/assets/fonts/Inter-Variable.woff2';
|
||||||
'Inter-Variable.woff2',
|
if (file_exists($font_path)) {
|
||||||
// 'InterDisplay-Variable.woff2',
|
printf(
|
||||||
];
|
'<link rel="preload" href="%s/assets/fonts/Inter-Variable.woff2" as="font" type="font/woff2" crossorigin>' . "\n",
|
||||||
|
esc_url(AV_THEME_URI)
|
||||||
foreach ($fonts as $font) {
|
);
|
||||||
$path = AV_THEME_DIR . '/assets/fonts/' . $font;
|
|
||||||
if (file_exists($path)) {
|
|
||||||
printf(
|
|
||||||
'<link rel="preload" href="%s/assets/fonts/%s" as="font" type="font/woff2" crossorigin>' . "\n",
|
|
||||||
esc_url(AV_THEME_URI),
|
|
||||||
esc_attr($font)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modular includes.
|
* Theme support + custom image sizes (used by Bricks media controls and
|
||||||
*
|
* Schema.org/OpenGraph emitters down the line).
|
||||||
* Each file is opt-in via file_exists() so partial scaffolds don't fatal.
|
|
||||||
*/
|
|
||||||
$av_includes = [
|
|
||||||
'inc/i18n.php', // av_lang(), av_t() — load FIRST.
|
|
||||||
'inc/cpt.php', // Custom post types: case_study, scenario.
|
|
||||||
'inc/seo.php', // Schema.org JSON-LD overrides.
|
|
||||||
'inc/form-handler.php', // Conversion form POST handler.
|
|
||||||
'inc/render-blocks.php', // Data array → core/html migrator (legacy, kept for fallback).
|
|
||||||
'inc/render-blocks-native.php', // Data array → native Gutenberg blocks (legacy POC).
|
|
||||||
'inc/render-bricks.php', // Data array → Bricks Builder native elements (current target).
|
|
||||||
'inc/shortcodes.php', // [av_form slug="..."] etc.
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($av_includes as $av_include) {
|
|
||||||
$path = AV_THEME_DIR . '/' . $av_include;
|
|
||||||
if (file_exists($path)) {
|
|
||||||
require_once $path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($av_include, $path);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Theme support and custom image sizes.
|
|
||||||
*
|
|
||||||
* Image sizes per Design Handoff section 8.3:
|
|
||||||
* - hero 2400×1350 (16:9)
|
|
||||||
* - card 800×600 (4:3)
|
|
||||||
* - thumb 400×300 (4:3)
|
|
||||||
*/
|
*/
|
||||||
add_action('after_setup_theme', function () {
|
add_action('after_setup_theme', function () {
|
||||||
add_theme_support('post-thumbnails');
|
add_theme_support('post-thumbnails');
|
||||||
@@ -133,41 +78,25 @@ add_action('after_setup_theme', function () {
|
|||||||
]);
|
]);
|
||||||
add_theme_support('responsive-embeds');
|
add_theme_support('responsive-embeds');
|
||||||
add_theme_support('align-wide');
|
add_theme_support('align-wide');
|
||||||
add_theme_support('editor-styles');
|
|
||||||
|
|
||||||
add_image_size('av-hero', 2400, 1350, true);
|
add_image_size('av-hero', 2400, 1350, true); // 16:9 hero
|
||||||
add_image_size('av-card', 800, 600, true);
|
add_image_size('av-card', 800, 600, true); // 4:3 card
|
||||||
add_image_size('av-thumb', 400, 300, true);
|
add_image_size('av-thumb', 400, 300, true); // 4:3 thumb
|
||||||
|
|
||||||
load_child_theme_textdomain('asterion-bricks', AV_THEME_DIR . '/languages');
|
load_child_theme_textdomain('asterion-bricks', AV_THEME_DIR . '/languages');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the WP admin bar on frontend for non-admins (cleaner preview screenshots).
|
* Modular includes — currently only CPT registration. SEO and other modules
|
||||||
|
* will be added as needed; the include loop is opt-in via file_exists().
|
||||||
*/
|
*/
|
||||||
add_action('after_setup_theme', function () {
|
$av_includes = [
|
||||||
if (! current_user_can('administrator')) {
|
'inc/cpt.php',
|
||||||
show_admin_bar(false);
|
];
|
||||||
|
foreach ($av_includes as $av_include) {
|
||||||
|
$path = AV_THEME_DIR . '/' . $av_include;
|
||||||
|
if (file_exists($path)) {
|
||||||
|
require_once $path;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
unset($av_include, $path);
|
||||||
/**
|
|
||||||
* Disable Gutenberg block library default styles on the public frontend only —
|
|
||||||
* we own typography end-to-end through tokens.css.
|
|
||||||
*
|
|
||||||
* Skipped when:
|
|
||||||
* - inside the Bricks Builder canvas (?bricks=run, ?brickspreview=1) — those
|
|
||||||
* screens rely on the WP-core stylesheets for their own UI / preview.
|
|
||||||
* - in the WP admin itself.
|
|
||||||
*/
|
|
||||||
add_action('wp_enqueue_scripts', function () {
|
|
||||||
if (is_admin()) return;
|
|
||||||
if (function_exists('bricks_is_builder') && bricks_is_builder()) return;
|
|
||||||
if (function_exists('bricks_is_frontend_builder_iframe') && bricks_is_frontend_builder_iframe()) return;
|
|
||||||
if (! empty($_GET['bricks']) || ! empty($_GET['brickspreview'])) return;
|
|
||||||
|
|
||||||
wp_dequeue_style('wp-block-library');
|
|
||||||
wp_dequeue_style('wp-block-library-theme');
|
|
||||||
wp_dequeue_style('global-styles');
|
|
||||||
wp_dequeue_style('classic-theme-styles');
|
|
||||||
}, 100);
|
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Global header
|
|
||||||
*
|
|
||||||
* Sticky header per Design Handoff section 3.5 :
|
|
||||||
* - 60px on mobile, 72px on desktop
|
|
||||||
* - Logo left, main nav center (desktop), CTA + lang switch right
|
|
||||||
* - Hamburger drawer on mobile
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html <?php language_attributes(); ?>>
|
|
||||||
<head>
|
|
||||||
<meta charset="<?php bloginfo('charset'); ?>">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
||||||
<meta name="theme-color" content="#0B1F3A">
|
|
||||||
<link rel="profile" href="https://gmpg.org/xfn/11">
|
|
||||||
<?php wp_head(); ?>
|
|
||||||
</head>
|
|
||||||
<body <?php body_class(); ?>>
|
|
||||||
<?php wp_body_open(); ?>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// In Bricks Builder mode, the parent theme's `template-parts/builder.php`
|
|
||||||
// calls get_header() / get_footer() to inherit our enqueues — but the
|
|
||||||
// builder UI then mounts directly into <body>. Emitting our own chrome
|
|
||||||
// here covers it visually, so we bail early.
|
|
||||||
if (function_exists('bricks_is_builder') && bricks_is_builder()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<a class="av-skip-link" href="#main"><?php echo esc_html(av_t('Skip to main content', 'asterion-bricks')); ?></a>
|
|
||||||
|
|
||||||
<header class="av-header" role="banner">
|
|
||||||
<div class="av-container av-header__inner">
|
|
||||||
|
|
||||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="av-header__logo" aria-label="Asterion VR — home">
|
|
||||||
ASTERION VR
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<nav class="av-nav" aria-label="<?php esc_attr_e('Primary', 'asterion-bricks'); ?>">
|
|
||||||
<ul class="av-nav__list" style="display:flex;gap:var(--space-6);list-style:none;margin:0;padding:0;">
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/solutions/')); ?>" class="av-nav__link" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<?php echo esc_html(av_t('Solutions', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/industries/')); ?>" class="av-nav__link" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<?php echo esc_html(av_t('Industries', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/technology/')); ?>" class="av-nav__link" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<?php echo esc_html(av_t('Technology', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/why-asterion/')); ?>" class="av-nav__link">
|
|
||||||
<?php echo esc_html(av_t('Why Asterion', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/insights/')); ?>" class="av-nav__link">
|
|
||||||
<?php echo esc_html(av_t('Insights', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="av-header__right">
|
|
||||||
<?php
|
|
||||||
// Render WPML language switcher when WPML is active and exposes languages.
|
|
||||||
$av_languages = apply_filters('wpml_active_languages', null, ['skip_missing' => 0]);
|
|
||||||
if (! empty($av_languages) && is_array($av_languages) && count($av_languages) > 1) :
|
|
||||||
?>
|
|
||||||
<div class="av-lang-switch" aria-label="<?php echo esc_attr(av_t('Language')); ?>">
|
|
||||||
<?php $av_first = true; foreach ($av_languages as $av_lang_data) :
|
|
||||||
if (! $av_first) : ?><span aria-hidden="true">·</span><?php endif; $av_first = false;
|
|
||||||
$av_code = strtoupper($av_lang_data['language_code']);
|
|
||||||
$av_url = esc_url($av_lang_data['url']);
|
|
||||||
$av_label = esc_html($av_lang_data['translated_name'] ?? $av_lang_data['native_name'] ?? $av_code);
|
|
||||||
if (! empty($av_lang_data['active'])) : ?>
|
|
||||||
<span class="av-lang-switch__current" aria-current="true"><?php echo esc_html($av_code); ?></span>
|
|
||||||
<?php else : ?>
|
|
||||||
<a href="<?php echo $av_url; ?>" class="av-lang-switch__other" hreflang="<?php echo esc_attr($av_lang_data['language_code']); ?>" lang="<?php echo esc_attr($av_lang_data['language_code']); ?>" aria-label="<?php echo esc_attr($av_label); ?>"><?php echo esc_html($av_code); ?></a>
|
|
||||||
<?php endif;
|
|
||||||
endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<a href="<?php echo esc_url(home_url('/request-demo/')); ?>" class="av-btn av-btn--primary av-btn--md">
|
|
||||||
<?php echo esc_html(av_t('Request a Demo', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<button type="button" class="av-drawer-toggle" aria-controls="av-drawer" aria-expanded="false" aria-label="<?php esc_attr_e('Open menu', 'asterion-bricks'); ?>">
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<aside id="av-drawer" class="av-drawer" aria-hidden="true">
|
|
||||||
<button type="button" class="av-drawer__close" aria-label="<?php esc_attr_e('Close menu', 'asterion-bricks'); ?>">×</button>
|
|
||||||
<nav class="av-drawer__nav" aria-label="<?php esc_attr_e('Mobile primary', 'asterion-bricks'); ?>">
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/solutions/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Solutions', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/industries/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Industries', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/technology/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Technology', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/why-asterion/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Why Asterion', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/insights/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Insights', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div class="av-drawer__cta-area">
|
|
||||||
<a href="<?php echo esc_url(home_url('/request-demo/')); ?>" class="av-btn av-btn--primary av-btn--lg" style="width:100%;">
|
|
||||||
<?php echo esc_html(av_t('Request a Demo', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main id="main" class="av-main">
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Conversion form handler
|
|
||||||
*
|
|
||||||
* Receives POSTs from /request-demo/, /request-te-kit/, /request-quote/,
|
|
||||||
* /contact/. Validates the WordPress nonce, screens the honeypot, sanitizes
|
|
||||||
* the payload, and emails the lead to the configured recipient.
|
|
||||||
*
|
|
||||||
* Hook : admin_post_av_form_submit (logged-out users use admin_post_nopriv).
|
|
||||||
*
|
|
||||||
* Lead destination defaults to AV_LEADS_RECIPIENT (override in wp-config.php),
|
|
||||||
* falling back to the WP admin email.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
if (! defined('AV_LEADS_RECIPIENT')) {
|
|
||||||
define('AV_LEADS_RECIPIENT', 'jerome.foucher@asterionvr.com');
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_handle_form_submit() {
|
|
||||||
|
|
||||||
// 1. Nonce
|
|
||||||
if (! isset($_POST['av_nonce']) || ! wp_verify_nonce(wp_unslash($_POST['av_nonce']), 'av_form_submit')) {
|
|
||||||
wp_safe_redirect(home_url('/?av_status=error'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Honeypot — bots fill anything
|
|
||||||
if (! empty($_POST['av_hp'])) {
|
|
||||||
// Pretend success so bots don't retry.
|
|
||||||
$redirect = isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/');
|
|
||||||
wp_safe_redirect(add_query_arg('av_status', 'success', $redirect));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Form data
|
|
||||||
$slug = isset($_POST['form_slug']) ? sanitize_key(wp_unslash($_POST['form_slug'])) : '';
|
|
||||||
$forms_data = include AV_THEME_DIR . '/inc/forms-data.php';
|
|
||||||
if (! isset($forms_data[$slug])) {
|
|
||||||
wp_safe_redirect(home_url('/?av_status=error'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$form = $forms_data[$slug];
|
|
||||||
|
|
||||||
// 4. Sanitize fields
|
|
||||||
$payload = [];
|
|
||||||
foreach ($form['fields'] as $field) {
|
|
||||||
$name = $field['name'];
|
|
||||||
if (! isset($_POST[$name])) {
|
|
||||||
$payload[$name] = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$raw = wp_unslash($_POST[$name]);
|
|
||||||
if ($field['type'] === 'email') {
|
|
||||||
$payload[$name] = sanitize_email($raw);
|
|
||||||
} elseif ($field['type'] === 'textarea') {
|
|
||||||
$payload[$name] = sanitize_textarea_field($raw);
|
|
||||||
} else {
|
|
||||||
$payload[$name] = sanitize_text_field($raw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Required-field validation
|
|
||||||
foreach ($form['fields'] as $field) {
|
|
||||||
if (! empty($field['required']) && empty($payload[$field['name']])) {
|
|
||||||
$redirect = isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/');
|
|
||||||
wp_safe_redirect(add_query_arg('av_status', 'error', $redirect));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Compose mail
|
|
||||||
$subject_prefix = '[Asterion VR · ' . $form['title'] . ']';
|
|
||||||
$subject = $subject_prefix . ' ' . ($payload['organization'] ?? $payload['full_name'] ?? '(unidentified lead)');
|
|
||||||
|
|
||||||
$body = "New lead from " . get_bloginfo('name') . " — form: " . $form['title'] . "\n";
|
|
||||||
$body .= "URL : " . (isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/')) . "\n";
|
|
||||||
$body .= str_repeat('-', 60) . "\n";
|
|
||||||
foreach ($form['fields'] as $field) {
|
|
||||||
$value = $payload[$field['name']] ?? '';
|
|
||||||
if ($value === '') continue;
|
|
||||||
$body .= str_pad($field['label'], 28) . ': ' . $value . "\n";
|
|
||||||
}
|
|
||||||
$body .= str_repeat('-', 60) . "\n";
|
|
||||||
$body .= "Submitted : " . current_time('mysql') . "\n";
|
|
||||||
$body .= "IP : " . (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '?') . "\n";
|
|
||||||
$body .= "User Agent: " . (isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '?') . "\n";
|
|
||||||
|
|
||||||
$headers = ['Content-Type: text/plain; charset=UTF-8'];
|
|
||||||
if (! empty($payload['email'])) {
|
|
||||||
$headers[] = 'Reply-To: ' . $payload['email'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$recipient = apply_filters('av_form_recipient', AV_LEADS_RECIPIENT, $slug, $payload);
|
|
||||||
|
|
||||||
$sent = wp_mail($recipient, $subject, $body, $headers);
|
|
||||||
|
|
||||||
// 7. Redirect
|
|
||||||
$redirect = isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/');
|
|
||||||
$status = $sent ? 'success' : 'error';
|
|
||||||
wp_safe_redirect(add_query_arg('av_status', $status, $redirect));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_action('admin_post_av_form_submit', 'av_handle_form_submit');
|
|
||||||
add_action('admin_post_nopriv_av_form_submit', 'av_handle_form_submit');
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — i18n helpers
|
|
||||||
*
|
|
||||||
* Lightweight WPML-aware language helpers used by Custom Templates to pick
|
|
||||||
* the right data file (e.g. solutions-data.php vs solutions-data-fr.php) and
|
|
||||||
* by header.php / footer.php to translate UI strings.
|
|
||||||
*
|
|
||||||
* We don't use WordPress .po/.mo loaders — WPML String Translation handles
|
|
||||||
* those — but we keep a small in-PHP map for the strings that aren't tied
|
|
||||||
* to data files (nav labels, generic CTAs, footer columns).
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Active language code, normalised to 'en' or 'fr'.
|
|
||||||
* Falls back to 'en' when WPML isn't active.
|
|
||||||
*/
|
|
||||||
function av_lang() {
|
|
||||||
$lang = apply_filters('wpml_current_language', null);
|
|
||||||
if (! $lang) {
|
|
||||||
$lang = function_exists('get_locale') ? substr(get_locale(), 0, 2) : 'en';
|
|
||||||
}
|
|
||||||
return ($lang === 'fr') ? 'fr' : 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the path to a localised data file.
|
|
||||||
* Example : av_data_file('solutions-data') returns
|
|
||||||
* '<theme>/inc/solutions-data.php' when EN
|
|
||||||
* '<theme>/inc/solutions-data-fr.php' when FR (if it exists)
|
|
||||||
*/
|
|
||||||
function av_data_file($basename) {
|
|
||||||
$base = AV_THEME_DIR . '/inc/' . $basename;
|
|
||||||
$localised = $base . '-' . av_lang() . '.php';
|
|
||||||
return file_exists($localised) ? $localised : $base . '.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a UI string. Falls back to the original on EN or when no entry
|
|
||||||
* matches in FR. Strings are kept in this file rather than .po/.mo so the
|
|
||||||
* theme stays self-contained.
|
|
||||||
*/
|
|
||||||
function av_t($string, $textdomain = 'asterion-bricks') {
|
|
||||||
if (av_lang() !== 'fr') {
|
|
||||||
return $string;
|
|
||||||
}
|
|
||||||
static $map = null;
|
|
||||||
if ($map === null) {
|
|
||||||
$map = [
|
|
||||||
// Nav + header
|
|
||||||
'Skip to main content' => 'Aller au contenu principal',
|
|
||||||
'Primary' => 'Principal',
|
|
||||||
'Mobile primary' => 'Principal mobile',
|
|
||||||
'Solutions' => 'Solutions',
|
|
||||||
'Industries' => 'Secteurs',
|
|
||||||
'Technology' => 'Technologie',
|
|
||||||
'Why Asterion' => 'Pourquoi Asterion',
|
|
||||||
'Insights' => 'Insights',
|
|
||||||
'Open menu' => 'Ouvrir le menu',
|
|
||||||
'Close menu' => 'Fermer le menu',
|
|
||||||
'Language' => 'Langue',
|
|
||||||
'Switch to French' => 'Passer en français',
|
|
||||||
'Switch to English' => 'Switch to English',
|
|
||||||
'Request a Demo' => 'Demander une démo',
|
|
||||||
|
|
||||||
// Footer
|
|
||||||
'Newsletter' => 'Newsletter',
|
|
||||||
'Our story' => 'Notre histoire',
|
|
||||||
'Customers' => 'Clients',
|
|
||||||
'Trust & Compliance' => 'Confiance et conformité',
|
|
||||||
'Partners' => 'Partenaires',
|
|
||||||
'Police' => 'Police',
|
|
||||||
'Special Forces' => 'Forces spéciales',
|
|
||||||
'Military' => 'Militaires',
|
|
||||||
'Firefighters' => 'Pompiers',
|
|
||||||
'Privacy' => 'Confidentialité',
|
|
||||||
'Terms' => 'Conditions',
|
|
||||||
'Cookies' => 'Cookies',
|
|
||||||
'Legal notice' => 'Mentions légales',
|
|
||||||
'All rights reserved.' => 'Tous droits réservés.',
|
|
||||||
'Insights from the front of immersive training. One email per month. No spam.'
|
|
||||||
=> 'Les enseignements du terrain de la formation immersive. Un e-mail par mois. Pas de spam.',
|
|
||||||
'Subscribe' => 'S’abonner',
|
|
||||||
'your@email.com' => 'vous@email.com',
|
|
||||||
'The modular, sovereign XR platform for European security forces. Train like the threat is now.'
|
|
||||||
=> 'La plateforme XR modulaire et souveraine pour les forces de sécurité européennes. Entraînez-vous comme si la menace était maintenant.',
|
|
||||||
|
|
||||||
// Generic CTAs / labels
|
|
||||||
'Read more' => 'Lire la suite',
|
|
||||||
'See all articles' => 'Voir tous les articles',
|
|
||||||
'Watch the 90-second film' => 'Voir le film de 90 secondes',
|
|
||||||
'Schedule a demo' => 'Planifier une démo',
|
|
||||||
'Request the kit' => 'Demander le kit',
|
|
||||||
'Get a quote' => 'Demander un devis',
|
|
||||||
'Send' => 'Envoyer',
|
|
||||||
'Send the message' => 'Envoyer le message',
|
|
||||||
'Apply for a T&E Kit' => 'Postuler pour un kit T&E',
|
|
||||||
'Schedule the demo' => 'Planifier la démo',
|
|
||||||
'Request the quote' => 'Demander le devis',
|
|
||||||
'Read the case study' => 'Lire l’étude de cas',
|
|
||||||
'Discuss a custom scenario' => 'Discuter d’un scénario sur mesure',
|
|
||||||
'Schedule a discovery call' => 'Planifier un appel de cadrage',
|
|
||||||
'Discuss your custom request' => 'Parler de votre besoin sur mesure',
|
|
||||||
'Become the next case study' => 'Devenez la prochaine étude de cas',
|
|
||||||
'Request a Trust Pack' => 'Demander un Trust Pack',
|
|
||||||
'Become a partner — apply' => 'Devenir partenaire — candidater',
|
|
||||||
'Compare with PROSERVE FLEX' => 'Comparer avec PROSERVE FLEX',
|
|
||||||
'Compare with PROSERVE ACADEMY' => 'Comparer avec PROSERVE ACADEMY',
|
|
||||||
'Comparison and upgrade' => 'Comparaison et évolution',
|
|
||||||
'Eligibility' => 'Éligibilité',
|
|
||||||
'Frequently asked' => 'Questions fréquentes',
|
|
||||||
'Quick answers.' => 'Réponses brèves.',
|
|
||||||
|
|
||||||
// Section eyebrows used on home / hubs / forms
|
|
||||||
'Modular XR training for security forces' => 'Formation XR modulaire pour les forces de sécurité',
|
|
||||||
'Trusted by' => 'Reconnu par',
|
|
||||||
'Trusted by security forces across Europe' => 'Adopté par les forces de sécurité en Europe',
|
|
||||||
'Voices from the front of immersive training.' => 'La parole des opérateurs.',
|
|
||||||
'Compare' => 'Comparer',
|
|
||||||
'The six pillars' => 'Les six piliers',
|
|
||||||
'Tell us about your unit.' => 'Parlez-nous de votre unité.',
|
|
||||||
'What you receive' => 'Ce que vous recevez',
|
|
||||||
'Other inquiries' => 'Autres demandes',
|
|
||||||
'How can we help?' => 'Comment pouvons-nous vous aider ?',
|
|
||||||
|
|
||||||
// Form widgets
|
|
||||||
'— select —' => '— choisir —',
|
|
||||||
'Your email' => 'Votre e-mail',
|
|
||||||
'Newsletter signup' => 'Inscription newsletter',
|
|
||||||
'Mobile drawer' => 'Tiroir mobile',
|
|
||||||
'Voices from the field' => 'La parole du terrain',
|
|
||||||
'Train how it really feels.' => 'Entraînez-vous au plus près du réel.',
|
|
||||||
'Train like the threat is now.' => 'Entraînez-vous comme si la menace était maintenant.',
|
|
||||||
'Three paths into the platform — pick the one that fits where you are.' => 'Trois portes d’entrée dans la plateforme — choisissez celle qui correspond à votre besoin.',
|
|
||||||
'A 30-minute tailored session with our team. Walk through the configuration that matches your needs.' => 'Une session sur mesure de 30 minutes avec notre équipe. Démonstration de la configuration adaptée à vos besoins.',
|
|
||||||
'A 30-minute tailored session with our team.' => 'Une session de 30 minutes avec notre équipe.',
|
|
||||||
'A PROSERVE case on loan at your facility for 5 to 10 days, with on-site support from our team.' => 'Un kit PROSERVE en prêt dans vos locaux pendant 5 à 10 jours, avec accompagnement sur site.',
|
|
||||||
'A PROSERVE case on loan at your facility for 5 to 10 days.' => 'Un kit PROSERVE en prêt dans vos locaux pendant 5 à 10 jours.',
|
|
||||||
'A detailed quote and deployment plan within 5 working days.' => 'Un devis détaillé et un plan de déploiement sous 5 jours ouvrés.',
|
|
||||||
'Ready to get hands-on?' => 'Prêt à passer à la pratique ?',
|
|
||||||
'Last updated:' => 'Dernière mise à jour :',
|
|
||||||
'Legal' => 'Légal',
|
|
||||||
'Placeholder.' => 'À compléter.',
|
|
||||||
'This page is to be drafted with legal counsel. The structure below outlines the sections expected.' =>
|
|
||||||
'Cette page sera rédigée avec votre conseil juridique. La structure ci-dessous liste les sections attendues.',
|
|
||||||
'Sections to include' => 'Sections à inclure',
|
|
||||||
'Police training' => 'Formation police',
|
|
||||||
'Special forces training' => 'Formation forces spéciales',
|
|
||||||
'Military training' => 'Formation militaire',
|
|
||||||
'Firefighter training' => 'Formation pompier',
|
|
||||||
'Insights from the front of immersive training.' => 'Les enseignements du terrain de la formation immersive.',
|
|
||||||
'See the full scenario library' => 'Voir la bibliothèque complète de scénarios',
|
|
||||||
'Inside the hardware' => 'Inside the hardware',
|
|
||||||
'Inside the software' => 'Inside the software',
|
|
||||||
'Inside weapons & tracking' => 'Inside weapons & tracking',
|
|
||||||
'Browse the scenario library' => 'Parcourir la bibliothèque',
|
|
||||||
'Inside the instructor cockpit' => 'Inside the instructor cockpit',
|
|
||||||
'Inside after-action review' => 'Inside after-action review',
|
|
||||||
'See how UWeD works' => 'Voir comment fonctionne UWeD',
|
|
||||||
'Explore After-Action Review' => 'Découvrir l’After-Action Review',
|
|
||||||
'Explore PROSERVE+ customization' => 'Découvrir la personnalisation PROSERVE+',
|
|
||||||
'Discover MY PROSERVE' => 'Découvrir MY PROSERVE',
|
|
||||||
'Discover PROSERVE FLEX' => 'Découvrir PROSERVE FLEX',
|
|
||||||
'Discover PROSERVE ACADEMY' => 'Découvrir PROSERVE ACADEMY',
|
|
||||||
'Request access' => 'Demander l’accès',
|
|
||||||
'Request a T&E Kit' => 'Demander un kit T&E',
|
|
||||||
'Request a Quote' => 'Demander un devis',
|
|
||||||
'Request a Demo for police use' => 'Demander une démo pour la police',
|
|
||||||
'Request a Demo for fire & emergency' => 'Demander une démo pour les pompiers',
|
|
||||||
'Thank you — your request was sent.' => 'Merci — votre demande a bien été envoyée.',
|
|
||||||
'We will reply within the timeframe stated above. Check your inbox (and spam) for confirmation.' =>
|
|
||||||
'Nous reviendrons vers vous dans les délais indiqués. Vérifiez votre boîte de réception (et vos spams) pour la confirmation.',
|
|
||||||
'Something went wrong.' => 'Une erreur est survenue.',
|
|
||||||
' Please try again, or email us directly at hello@asterionvr.com.' =>
|
|
||||||
' Veuillez réessayer, ou écrivez-nous directement à hello@asterionvr.com.',
|
|
||||||
'Editorial blog launches with the site go-live. The 12-month content calendar is in the Strategy & Content brief, section B.4.' =>
|
|
||||||
'Le blog éditorial démarrera avec la mise en ligne du site. Le calendrier sur 12 mois figure dans le livrable Stratégie & Contenu, partie B.4.',
|
|
||||||
'Editorial blog launches with the site go-live. The 12-month content calendar is in the Strategy & Content brief, section B.4. New articles will appear here as soon as they ship.' =>
|
|
||||||
'Le blog éditorial démarrera avec la mise en ligne du site. Le calendrier sur 12 mois figure dans le livrable Stratégie & Contenu, partie B.4. Les articles apparaîtront ici dès leur publication.',
|
|
||||||
'Subscribe — one article per month.' => 'Abonnez-vous — un article par mois.',
|
|
||||||
'Notes from the front of immersive training.' => 'Les enseignements du terrain de la formation immersive.',
|
|
||||||
'Doctrine. Training science. Instructor stories. AAR data. Comparisons of VR vs live training. Found something useful? It probably came from a customer conversation. We publish what we learn.' =>
|
|
||||||
'Doctrine. Science de la formation. Récits d’instructeurs. Données AAR. Comparaisons VR vs entraînement réel. Quelque chose d’utile ? Cela vient probablement d’un échange client. Nous publions ce que nous apprenons.',
|
|
||||||
'Everything to evaluate, justify, and procure.' => 'Tout pour évaluer, justifier, acheter.',
|
|
||||||
'Whether you are building an internal pitch, drafting a procurement specification, or briefing your team — these resources are designed to help.' =>
|
|
||||||
'Que vous prépariez un argumentaire interne, rédigiez un cahier des charges ou briefiez vos équipes — ces ressources sont là pour vous aider.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return $map[$string] ?? $string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience: echo a translated string.
|
|
||||||
*/
|
|
||||||
function av_te($string) {
|
|
||||||
echo esc_html(av_t($string));
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Native Gutenberg block renderer (POC for Industries)
|
|
||||||
*
|
|
||||||
* Generates strictly-standard Gutenberg blocks (core/group, core/heading,
|
|
||||||
* core/paragraph, core/columns, core/column, core/buttons, core/button,
|
|
||||||
* core/list) so that:
|
|
||||||
*
|
|
||||||
* 1. Pages are editable in the WP admin block editor — every text node
|
|
||||||
* is a real block, not a raw HTML wrapper.
|
|
||||||
* 2. "Edit with Bricks" converts these to Bricks elements (Container,
|
|
||||||
* Heading, Text-Basic, Button, …) per Bricks Builder docs.
|
|
||||||
* 3. WPML's Translation Editor exposes one entry per block.
|
|
||||||
*
|
|
||||||
* Convention: every block carries a `className` attribute matching our
|
|
||||||
* existing `av-*` CSS classes so the rendered HTML inherits the design
|
|
||||||
* system styles.
|
|
||||||
*
|
|
||||||
* Public API (POC, will replace inc/render-blocks.php once validated):
|
|
||||||
* av_n_render_industries_overview() → string (single hub page)
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a Gutenberg block array (the shape `serialize_blocks()` expects).
|
|
||||||
* `innerHTML` is the wrapper template — Gutenberg uses it as a delimiter
|
|
||||||
* map, with NULL entries marking where children get inlined.
|
|
||||||
*/
|
|
||||||
function av_n_block($name, $attrs = [], $inner_blocks = [], $inner_html = '') {
|
|
||||||
return [
|
|
||||||
'blockName' => $name,
|
|
||||||
'attrs' => $attrs,
|
|
||||||
'innerBlocks' => $inner_blocks,
|
|
||||||
'innerHTML' => $inner_html,
|
|
||||||
'innerContent' => [$inner_html],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group block (tagName configurable). Used for sections, containers, cards.
|
|
||||||
* Children are kept as innerBlocks; innerContent uses NULL placeholders so
|
|
||||||
* Gutenberg slots them in at render time.
|
|
||||||
*/
|
|
||||||
function av_n_group($attrs, $children, $tag = 'div') {
|
|
||||||
$attrs = array_merge(['tagName' => $tag], $attrs);
|
|
||||||
$class = trim('wp-block-group ' . ($attrs['className'] ?? ''));
|
|
||||||
|
|
||||||
// Build innerContent: opening tag, then NULL per child, then closing tag.
|
|
||||||
$inner_content = ['<' . $tag . ' class="' . esc_attr($class) . '">'];
|
|
||||||
foreach ($children as $child) {
|
|
||||||
$inner_content[] = null;
|
|
||||||
}
|
|
||||||
$inner_content[] = '</' . $tag . '>';
|
|
||||||
|
|
||||||
return [
|
|
||||||
'blockName' => 'core/group',
|
|
||||||
'attrs' => $attrs,
|
|
||||||
'innerBlocks' => $children,
|
|
||||||
'innerHTML' => '<' . $tag . ' class="' . esc_attr($class) . '"></' . $tag . '>',
|
|
||||||
'innerContent' => $inner_content,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_n_heading($text, $level = 2, $class = '') {
|
|
||||||
$tag = 'h' . max(1, min(6, (int) $level));
|
|
||||||
$cls = trim('wp-block-heading ' . $class);
|
|
||||||
$html = '<' . $tag . ' class="' . esc_attr($cls) . '">' . esc_html($text) . '</' . $tag . '>';
|
|
||||||
|
|
||||||
return [
|
|
||||||
'blockName' => 'core/heading',
|
|
||||||
'attrs' => array_filter([
|
|
||||||
'level' => (int) $level,
|
|
||||||
'className' => $class ?: null,
|
|
||||||
]),
|
|
||||||
'innerBlocks' => [],
|
|
||||||
'innerHTML' => $html,
|
|
||||||
'innerContent' => [$html],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_n_paragraph($text, $class = '') {
|
|
||||||
$html = $class
|
|
||||||
? '<p class="' . esc_attr($class) . '">' . esc_html($text) . '</p>'
|
|
||||||
: '<p>' . esc_html($text) . '</p>';
|
|
||||||
|
|
||||||
return [
|
|
||||||
'blockName' => 'core/paragraph',
|
|
||||||
'attrs' => $class ? ['className' => $class] : [],
|
|
||||||
'innerBlocks' => [],
|
|
||||||
'innerHTML' => $html,
|
|
||||||
'innerContent' => [$html],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_n_button($label, $href, $variants = 'av-btn av-btn--primary av-btn--lg') {
|
|
||||||
$btn_html = '<div class="wp-block-button ' . esc_attr($variants) . '"><a class="wp-block-button__link wp-element-button" href="' . esc_url($href) . '">' . esc_html($label) . '</a></div>';
|
|
||||||
|
|
||||||
return [
|
|
||||||
'blockName' => 'core/button',
|
|
||||||
'attrs' => ['className' => $variants],
|
|
||||||
'innerBlocks' => [],
|
|
||||||
'innerHTML' => $btn_html,
|
|
||||||
'innerContent' => [$btn_html],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_n_buttons_row($buttons, $class = 'av-hero__ctas') {
|
|
||||||
$opening = '<div class="wp-block-buttons ' . esc_attr($class) . '">';
|
|
||||||
$closing = '</div>';
|
|
||||||
$inner_content = [$opening];
|
|
||||||
foreach ($buttons as $b) {
|
|
||||||
$inner_content[] = null;
|
|
||||||
}
|
|
||||||
$inner_content[] = $closing;
|
|
||||||
return [
|
|
||||||
'blockName' => 'core/buttons',
|
|
||||||
'attrs' => ['className' => $class],
|
|
||||||
'innerBlocks' => $buttons,
|
|
||||||
'innerHTML' => $opening . $closing,
|
|
||||||
'innerContent' => $inner_content,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_n_column($children, $width = null) {
|
|
||||||
$attrs = $width ? ['width' => $width] : [];
|
|
||||||
$opening = '<div class="wp-block-column"' . ($width ? ' style="flex-basis:' . esc_attr($width) . '"' : '') . '>';
|
|
||||||
$closing = '</div>';
|
|
||||||
$inner_content = [$opening];
|
|
||||||
foreach ($children as $c) $inner_content[] = null;
|
|
||||||
$inner_content[] = $closing;
|
|
||||||
return [
|
|
||||||
'blockName' => 'core/column',
|
|
||||||
'attrs' => $attrs,
|
|
||||||
'innerBlocks' => $children,
|
|
||||||
'innerHTML' => $opening . $closing,
|
|
||||||
'innerContent' => $inner_content,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_n_columns($columns, $class = 'av-grid-4') {
|
|
||||||
$opening = '<div class="wp-block-columns ' . esc_attr($class) . '">';
|
|
||||||
$closing = '</div>';
|
|
||||||
$inner_content = [$opening];
|
|
||||||
foreach ($columns as $c) $inner_content[] = null;
|
|
||||||
$inner_content[] = $closing;
|
|
||||||
return [
|
|
||||||
'blockName' => 'core/columns',
|
|
||||||
'attrs' => array_filter(['className' => $class, 'isStackedOnMobile' => true]),
|
|
||||||
'innerBlocks' => $columns,
|
|
||||||
'innerHTML' => $opening . $closing,
|
|
||||||
'innerContent' => $inner_content,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========================================================================== */
|
|
||||||
/* Industries overview hub — proof of concept */
|
|
||||||
/* ========================================================================== */
|
|
||||||
|
|
||||||
function av_n_render_industries_overview() {
|
|
||||||
$home = home_url('/');
|
|
||||||
|
|
||||||
$industries = [
|
|
||||||
['eyebrow' => 'National & Municipal', 'title' => 'Police', 'excerpt' => 'Vehicle stops, identity checks, urban CQB, de-escalation, riot containment. Built for the realities of contemporary policing.', 'href' => '/industries/police/', 'cta' => 'Police training'],
|
|
||||||
['eyebrow' => 'Tactical units', 'title' => 'Special Forces', 'excerpt' => 'Hostage rescue, dynamic entries, hostile crowd extraction, sniper-spotter coordination. Built for the units where every error has a name.', 'href' => '/industries/special-forces/', 'cta' => 'Special forces training'],
|
|
||||||
['eyebrow' => 'Armed forces', 'title' => 'Military', 'excerpt' => 'MOUT, convoy escort, FOB defense, IED awareness, joint operation drills. Built for armed forces that deploy fast and adapt faster.', 'href' => '/industries/military/', 'cta' => 'Military training'],
|
|
||||||
['eyebrow' => 'Emergency services', 'title' => 'Firefighters', 'excerpt' => "Structure fires, hazmat, multi-casualty triage, confined-space rescue, industrial accidents. Built for first responders who can't rehearse the worst day at full scale.", 'href' => '/industries/firefighters/', 'cta' => 'Firefighter training'],
|
|
||||||
];
|
|
||||||
|
|
||||||
/* ----- Hero (section / container / inner) ----- */
|
|
||||||
$hero_inner = av_n_group(
|
|
||||||
['className' => 'av-container av-hero__inner', 'layout' => ['type' => 'constrained']],
|
|
||||||
[
|
|
||||||
av_n_paragraph('Industries', 'av-hero__eyebrow'),
|
|
||||||
av_n_heading('Built with operators, for operators.', 1, 'av-hero__headline'),
|
|
||||||
av_n_paragraph(
|
|
||||||
'PROSERVE™ is not a generic VR platform repackaged for the public sector. Every scenario, every weapon profile, every instructor metric was specified by serving and former operators in close partnership with our engineering team.',
|
|
||||||
'av-hero__sub'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
'div'
|
|
||||||
);
|
|
||||||
$hero = av_n_group(
|
|
||||||
['className' => 'av-hero'],
|
|
||||||
[$hero_inner],
|
|
||||||
'section'
|
|
||||||
);
|
|
||||||
|
|
||||||
/* ----- Cards section ----- */
|
|
||||||
$card_columns = [];
|
|
||||||
foreach ($industries as $ind) {
|
|
||||||
$card_inner = av_n_group(
|
|
||||||
['className' => 'av-card__body'],
|
|
||||||
[
|
|
||||||
av_n_paragraph($ind['eyebrow'], 'av-card__eyebrow'),
|
|
||||||
av_n_heading($ind['title'], 2, 'av-card__title'),
|
|
||||||
av_n_paragraph($ind['excerpt'], 'av-card__excerpt'),
|
|
||||||
av_n_buttons_row(
|
|
||||||
[av_n_button($ind['cta'], $home . ltrim($ind['href'], '/'), 'av-btn av-btn--secondary av-btn--md')],
|
|
||||||
'av-card__cta'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
'div'
|
|
||||||
);
|
|
||||||
$card = av_n_group(
|
|
||||||
['className' => 'av-card'],
|
|
||||||
[$card_inner],
|
|
||||||
'article'
|
|
||||||
);
|
|
||||||
$card_columns[] = av_n_column([$card]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cards_grid = av_n_columns($card_columns, 'av-grid-4');
|
|
||||||
|
|
||||||
$section_container = av_n_group(
|
|
||||||
['className' => 'av-container', 'layout' => ['type' => 'constrained']],
|
|
||||||
[$cards_grid],
|
|
||||||
'div'
|
|
||||||
);
|
|
||||||
$section = av_n_group(
|
|
||||||
['className' => 'av-section'],
|
|
||||||
[$section_container],
|
|
||||||
'section'
|
|
||||||
);
|
|
||||||
|
|
||||||
return serialize_blocks([$hero, $section]);
|
|
||||||
}
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Data → Gutenberg blocks renderer
|
|
||||||
*
|
|
||||||
* Converts the structured PHP arrays in inc/<section>-data.php into a
|
|
||||||
* Gutenberg block payload (`post_content`) so that :
|
|
||||||
*
|
|
||||||
* - Pages are editable in the WP admin block editor
|
|
||||||
* - Bricks Builder can pick them up via « Edit with Bricks » (Bricks reads
|
|
||||||
* post_content and converts core blocks → Bricks elements on save)
|
|
||||||
* - WPML's Translation Editor exposes every block for translation
|
|
||||||
*
|
|
||||||
* Strategy
|
|
||||||
* --------
|
|
||||||
* We generate Gutenberg-valid HTML using a mix of native blocks
|
|
||||||
* (`core/heading`, `core/paragraph`, `core/list`, `core/buttons`,
|
|
||||||
* `core/columns`) wrapped in `core/group` containers carrying our
|
|
||||||
* `av-*` classes. Visually-heavy items (hero, conversion banner,
|
|
||||||
* complex cards) fall back to `core/html` blocks so the rendering
|
|
||||||
* stays pixel-identical to the Custom Template version, while still
|
|
||||||
* being individually translatable in WPML.
|
|
||||||
*
|
|
||||||
* Public API
|
|
||||||
* ----------
|
|
||||||
* av_render_solution_blocks($data) → string (post_content)
|
|
||||||
* av_render_industry_blocks($data) → string
|
|
||||||
* av_render_technology_blocks($data) → string (alias of industry)
|
|
||||||
* av_render_editorial_blocks($data) → string (alias of industry)
|
|
||||||
* av_render_form_blocks($data, $slug) → string
|
|
||||||
* av_render_overview_blocks($cards, $hero, $bg) → string (hubs)
|
|
||||||
* av_render_home_blocks() → string
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Block helpers */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_block_html($html) {
|
|
||||||
$html = trim($html);
|
|
||||||
return "<!-- wp:html -->\n{$html}\n<!-- /wp:html -->\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_heading($text, $level = 2, $class = '') {
|
|
||||||
$tag = 'h' . max(1, min(6, (int) $level));
|
|
||||||
$class = $class ? ' ' . esc_attr($class) : '';
|
|
||||||
$args = json_encode(['level' => (int) $level, 'className' => trim($class)]);
|
|
||||||
return "<!-- wp:heading {$args} -->\n<{$tag} class=\"wp-block-heading{$class}\">" . esc_html($text) . "</{$tag}>\n<!-- /wp:heading -->\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_paragraph($text, $class = '') {
|
|
||||||
$args = $class ? json_encode(['className' => $class]) . ' ' : '';
|
|
||||||
$cls = $class ? ' class="' . esc_attr($class) . '"' : '';
|
|
||||||
return "<!-- wp:paragraph {$args}-->\n<p{$cls}>" . esc_html($text) . "</p>\n<!-- /wp:paragraph -->\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_list($items) {
|
|
||||||
$out = "<!-- wp:list -->\n<ul>\n";
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$out .= "<!-- wp:list-item -->\n<li>" . esc_html($item) . "</li>\n<!-- /wp:list-item -->\n";
|
|
||||||
}
|
|
||||||
$out .= "</ul>\n<!-- /wp:list -->\n\n";
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_button($label, $href, $variant = 'primary', $on_dark = false) {
|
|
||||||
$href = esc_url($href);
|
|
||||||
$label = esc_html($label);
|
|
||||||
$cls = 'av-btn av-btn--' . $variant . ($on_dark ? ' av-btn--on-dark' : '') . ' av-btn--lg';
|
|
||||||
return "<!-- wp:html -->\n<a class=\"{$cls}\" href=\"{$href}\">{$label}</a>\n<!-- /wp:html -->\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_buttons_row($ctas, $on_dark = false, $home_url = '') {
|
|
||||||
if (empty($ctas)) return '';
|
|
||||||
$out = "<!-- wp:html -->\n<div style=\"display:flex;flex-wrap:wrap;gap:var(--space-4);\">\n";
|
|
||||||
foreach ($ctas as $cta) {
|
|
||||||
$href = $home_url . ltrim($cta['href'], '/');
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : ($on_dark ? 'av-btn--on-dark av-btn--secondary' : 'av-btn--secondary');
|
|
||||||
$out .= '<a class="av-btn ' . esc_attr($variant) . ' av-btn--lg" href="' . esc_url($href) . '">' . esc_html($cta['label']) . "</a>\n";
|
|
||||||
}
|
|
||||||
$out .= "</div>\n<!-- /wp:html -->\n\n";
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Hero blocks */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_render_solution_hero($data, $home_url) {
|
|
||||||
$eyebrow = esc_html($data['eyebrow']);
|
|
||||||
$title = esc_html($data['title']);
|
|
||||||
$tagline = esc_html($data['tagline']);
|
|
||||||
$lead = esc_html($data['hero_lead']);
|
|
||||||
$ctas = '';
|
|
||||||
foreach ($data['ctas'] as $cta) {
|
|
||||||
$href = $home_url . ltrim($cta['href'], '/');
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary av-btn--on-dark';
|
|
||||||
$ctas .= '<a class="av-btn ' . $variant . ' av-btn--lg" href="' . esc_url($href) . '">' . esc_html($cta['label']) . "</a>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = <<<HTML
|
|
||||||
<section class="av-hero">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:radial-gradient(ellipse at 70% 30%, rgba(201,164,90,0.15), transparent 60%),linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow">{$eyebrow}</p>
|
|
||||||
<h1 class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);">{$title}™ — {$tagline}</h1>
|
|
||||||
<p class="av-hero__sub">{$lead}</p>
|
|
||||||
<div class="av-hero__ctas">{$ctas}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
return av_block_html($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_industry_hero($data, $home_url) {
|
|
||||||
$eyebrow = esc_html($data['eyebrow']);
|
|
||||||
$tagline = esc_html($data['tagline']);
|
|
||||||
$lead = esc_html($data['hero_lead']);
|
|
||||||
$ctas = '';
|
|
||||||
foreach ($data['ctas'] as $cta) {
|
|
||||||
$href = $home_url . ltrim($cta['href'], '/');
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary av-btn--on-dark';
|
|
||||||
$ctas .= '<a class="av-btn ' . $variant . ' av-btn--lg" href="' . esc_url($href) . '">' . esc_html($cta['label']) . "</a>\n";
|
|
||||||
}
|
|
||||||
$html = <<<HTML
|
|
||||||
<section class="av-hero">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:radial-gradient(ellipse at 50% 40%, rgba(11,31,58,0.4), transparent 65%),linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #1F3252 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow">{$eyebrow}</p>
|
|
||||||
<h1 class="av-hero__headline">{$tagline}</h1>
|
|
||||||
<p class="av-hero__sub">{$lead}</p>
|
|
||||||
<div class="av-hero__ctas">{$ctas}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
return av_block_html($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Section renderer (shared by solution / industry / technology / */
|
|
||||||
/* editorial detail pages) */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_render_data_section($section, $is_dark, $home_url) {
|
|
||||||
$section_class = 'av-section' . ($is_dark ? ' av-section--dark' : '');
|
|
||||||
$eye_class = 'av-eyebrow' . ($is_dark ? ' av-eyebrow--gold' : '');
|
|
||||||
$eyebrow = esc_html($section['eyebrow'] ?? '');
|
|
||||||
$title = esc_html($section['title'] ?? '');
|
|
||||||
$lead = ! empty($section['lead']) ? '<p class="av-section__lead">' . esc_html($section['lead']) . '</p>' : '';
|
|
||||||
|
|
||||||
$head_html = <<<HTML
|
|
||||||
<section class="{$section_class}">
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="{$eye_class}">{$eyebrow}</span>
|
|
||||||
<h2 class="av-section__title">{$title}</h2>
|
|
||||||
{$lead}
|
|
||||||
</div>
|
|
||||||
HTML;
|
|
||||||
|
|
||||||
$body = '';
|
|
||||||
|
|
||||||
switch ($section['type']) {
|
|
||||||
case 'prose':
|
|
||||||
$body = '<p class="av-section__lead" style="max-width:72ch;font-size:var(--text-body-lg);">' . esc_html($section['body'] ?? '') . '</p>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'cards':
|
|
||||||
$body = '<div class="av-grid-3">';
|
|
||||||
foreach ($section['items'] as $card) {
|
|
||||||
$card_class = 'av-card' . ($is_dark ? ' is-on-dark' : '') . ' av-card--feature';
|
|
||||||
$body .= '<article class="' . $card_class . '"><div class="av-card__body">';
|
|
||||||
$body .= '<h3 class="av-card__title" style="font-size:var(--text-h4);">' . esc_html($card['title']) . '</h3>';
|
|
||||||
$body .= '<p class="av-card__excerpt">' . esc_html($card['body']) . '</p>';
|
|
||||||
$body .= '</div></article>';
|
|
||||||
}
|
|
||||||
$body .= '</div>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'list':
|
|
||||||
$color = $is_dark ? 'color:var(--color-text-on-dark-muted);' : 'color:var(--color-text-secondary);';
|
|
||||||
$body = '<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);max-width:72ch;font-size:var(--text-body-lg);line-height:var(--leading-body-lg);' . $color . '">';
|
|
||||||
foreach ($section['items'] as $item) {
|
|
||||||
$body .= '<li style="padding-left:var(--space-6);position:relative;"><span aria-hidden="true" style="position:absolute;left:0;top:0.6em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>' . esc_html($item) . '</li>';
|
|
||||||
}
|
|
||||||
$body .= '</ul>';
|
|
||||||
if (! empty($section['cta'])) {
|
|
||||||
$href = $home_url . ltrim($section['cta']['href'], '/');
|
|
||||||
$linkcls = 'av-link' . ($is_dark ? ' av-link--on-dark' : '') . ' av-link--with-arrow';
|
|
||||||
$body .= '<p style="margin-top:var(--space-8);"><a class="' . $linkcls . '" href="' . esc_url($href) . '">' . esc_html($section['cta']['label']) . '</a></p>';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'cta':
|
|
||||||
$body = '<p class="av-section__lead" style="max-width:72ch;margin-bottom:var(--space-6);">' . esc_html($section['body'] ?? '') . '</p>';
|
|
||||||
if (! empty($section['ctas'])) {
|
|
||||||
$body .= '<div style="display:flex;flex-wrap:wrap;gap:var(--space-4);">';
|
|
||||||
foreach ($section['ctas'] as $cta) {
|
|
||||||
$href = $home_url . ltrim($cta['href'], '/');
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : ($is_dark ? 'av-btn--on-dark av-btn--secondary' : 'av-btn--secondary');
|
|
||||||
$body .= '<a class="av-btn ' . $variant . ' av-btn--lg" href="' . esc_url($href) . '">' . esc_html($cta['label']) . "</a>";
|
|
||||||
}
|
|
||||||
$body .= '</div>';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$foot_html = "</div></section>";
|
|
||||||
return av_block_html($head_html . $body . $foot_html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Conversion banner (shared) */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_render_conversion_banner($home_url) {
|
|
||||||
$html = <<<HTML
|
|
||||||
<section class="av-conversion-banner">
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-conversion-banner__head">
|
|
||||||
<h2 class="av-conversion-banner__title">Train like the threat is now.</h2>
|
|
||||||
<p class="av-conversion-banner__lead">Three paths into the platform — pick the one that fits where you are.</p>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__grid">
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a Demo</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A 30-minute tailored session with our team.</p>
|
|
||||||
<a href="{$home_url}request-demo/" class="av-btn av-btn--primary av-btn--md">Schedule a demo</a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a T&E Kit</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A PROSERVE case on loan at your facility for 5 to 10 days.</p>
|
|
||||||
<a href="{$home_url}request-te-kit/" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md">Request the kit</a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a Quote</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A detailed quote and deployment plan within 5 working days.</p>
|
|
||||||
<a href="{$home_url}request-quote/" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md">Get a quote</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
return av_block_html($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Top-level renderers */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_render_solution_blocks($data) {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$out = av_render_solution_hero($data, $home_url);
|
|
||||||
foreach ($data['sections'] as $idx => $section) {
|
|
||||||
$out .= av_render_data_section($section, ($idx % 2 === 1), $home_url);
|
|
||||||
}
|
|
||||||
if (! empty($data['upsell'])) {
|
|
||||||
$upsell_html = '<section class="av-section" style="background-color:var(--color-bg-subtle);"><div class="av-container">';
|
|
||||||
$upsell_html .= '<div class="av-section__head">';
|
|
||||||
$upsell_html .= '<span class="av-eyebrow">Comparison and upgrade</span>';
|
|
||||||
$upsell_html .= '<h2 class="av-section__title">' . esc_html($data['upsell']['title']) . '</h2>';
|
|
||||||
$upsell_html .= '<p class="av-section__lead">' . esc_html($data['upsell']['body']) . '</p>';
|
|
||||||
$upsell_html .= '</div><div style="display:flex;flex-wrap:wrap;gap:var(--space-4);">';
|
|
||||||
foreach ($data['upsell']['links'] as $link) {
|
|
||||||
$upsell_html .= '<a class="av-btn av-btn--secondary av-btn--md" href="' . esc_url($home_url . ltrim($link['href'], '/')) . '">' . esc_html($link['label']) . "</a>";
|
|
||||||
}
|
|
||||||
$upsell_html .= '</div></div></section>';
|
|
||||||
$out .= av_block_html($upsell_html);
|
|
||||||
}
|
|
||||||
$out .= av_render_conversion_banner($home_url);
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_industry_blocks($data) {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$out = av_render_industry_hero($data, $home_url);
|
|
||||||
foreach ($data['sections'] as $idx => $section) {
|
|
||||||
$out .= av_render_data_section($section, ($idx % 2 === 1), $home_url);
|
|
||||||
}
|
|
||||||
$out .= av_render_conversion_banner($home_url);
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_technology_blocks($data) {
|
|
||||||
return av_render_industry_blocks($data); // identical schema
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_editorial_blocks($data) {
|
|
||||||
return av_render_industry_blocks($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_form_blocks($data, $slug) {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
|
|
||||||
// Hero
|
|
||||||
$eyebrow = esc_html($data['eyebrow']);
|
|
||||||
$tagline = esc_html($data['tagline']);
|
|
||||||
$lead = esc_html($data['hero_lead']);
|
|
||||||
$shortcuts_html = '';
|
|
||||||
if (! empty($data['shortcuts'])) {
|
|
||||||
$shortcuts_html .= '<div class="av-hero__ctas">';
|
|
||||||
foreach ($data['shortcuts'] as $sc) {
|
|
||||||
$variant = ($sc['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--on-dark av-btn--secondary';
|
|
||||||
$shortcuts_html .= '<a class="av-btn ' . $variant . ' av-btn--md" href="' . esc_url($home_url . ltrim($sc['href'], '/')) . '">' . esc_html($sc['label']) . '</a>';
|
|
||||||
}
|
|
||||||
$shortcuts_html .= '</div>';
|
|
||||||
}
|
|
||||||
$hero_html = <<<HTML
|
|
||||||
<section class="av-hero">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:radial-gradient(ellipse at 50% 30%, rgba(201,164,90,0.12), transparent 60%),linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow">{$eyebrow}</p>
|
|
||||||
<h1 class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);">{$tagline}</h1>
|
|
||||||
<p class="av-hero__sub">{$lead}</p>
|
|
||||||
{$shortcuts_html}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
$out = av_block_html($hero_html);
|
|
||||||
|
|
||||||
// The form itself stays a shortcode — added by template wrapper instead.
|
|
||||||
// To keep the page editable, we drop a placeholder shortcode that the
|
|
||||||
// child theme's `av_form` shortcode handler renders on the front-end.
|
|
||||||
$out .= av_block_html('<div class="av-form-placeholder">[av_form slug="' . esc_attr($slug) . '"]</div>');
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_overview_blocks($cards, $hero_eyebrow, $hero_title, $hero_sub) {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$hero_html = <<<HTML
|
|
||||||
<section class="av-hero">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow">{$hero_eyebrow}</p>
|
|
||||||
<h1 class="av-hero__headline">{$hero_title}</h1>
|
|
||||||
<p class="av-hero__sub">{$hero_sub}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
$out = av_block_html($hero_html);
|
|
||||||
|
|
||||||
$cards_html = '<section class="av-section"><div class="av-container"><div class="av-grid-3">';
|
|
||||||
foreach ($cards as $c) {
|
|
||||||
$href = $home_url . ltrim($c['href'], '/');
|
|
||||||
$cards_html .= '<article class="av-card"><a href="' . esc_url($href) . '" class="av-card__media" tabindex="-1" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);display:block;"></a>';
|
|
||||||
$cards_html .= '<div class="av-card__body">';
|
|
||||||
$cards_html .= '<span class="av-card__eyebrow">' . esc_html($c['eyebrow'] ?? '') . '</span>';
|
|
||||||
$cards_html .= '<h2 class="av-card__title"><a href="' . esc_url($href) . '" style="color:inherit;text-decoration:none;">' . esc_html($c['title']) . '</a></h2>';
|
|
||||||
$cards_html .= '<p class="av-card__excerpt">' . esc_html($c['excerpt']) . '</p>';
|
|
||||||
$cards_html .= '<div class="av-card__cta"><a href="' . esc_url($href) . '" class="av-link av-link--with-arrow">' . esc_html($c['cta']) . '</a></div>';
|
|
||||||
$cards_html .= '</div></article>';
|
|
||||||
}
|
|
||||||
$cards_html .= '</div></div></section>';
|
|
||||||
$out .= av_block_html($cards_html);
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Bricks Builder native data renderer (POC: Industries)
|
|
||||||
*
|
|
||||||
* Generates the array of element descriptors that Bricks Builder expects in
|
|
||||||
* the post meta `_bricks_page_content_2` (constant `BRICKS_DB_PAGE_CONTENT`).
|
|
||||||
*
|
|
||||||
* Schema, confirmed by reading Bricks parent theme source
|
|
||||||
* (`includes/password-protection.php` and `includes/elements/base.php`):
|
|
||||||
*
|
|
||||||
* [
|
|
||||||
* [
|
|
||||||
* 'id' => string, // unique, via Bricks\Helpers::generate_random_id(false)
|
|
||||||
* 'name' => 'section' | 'container' | 'block' | 'heading' | 'text-basic' | 'button' | …,
|
|
||||||
* 'parent' => 0 | string, // 0 = page root, else parent's id
|
|
||||||
* 'children' => [string, …], // optional, for nestable elements
|
|
||||||
* 'settings' => [
|
|
||||||
* 'text' => 'Hello',
|
|
||||||
* 'tag' => 'h1' | 'h2' | …,
|
|
||||||
* 'link' => ['type' => 'external', 'url' => 'https://…'],
|
|
||||||
* '_cssClasses' => 'av-hero',
|
|
||||||
* …
|
|
||||||
* ],
|
|
||||||
* ],
|
|
||||||
* …
|
|
||||||
* ]
|
|
||||||
*
|
|
||||||
* Once stored, opening the page in WP admin and clicking "Edit with Bricks"
|
|
||||||
* surfaces every element in the Bricks builder canvas, fully editable.
|
|
||||||
*
|
|
||||||
* Frontend rendering is handled by Bricks itself — our `page.php` simply
|
|
||||||
* lets Bricks take over via its `template_include` filter.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a Bricks-compatible random element ID.
|
|
||||||
* Falls back to a local md5 prefix if the Bricks namespace isn't loaded yet.
|
|
||||||
*/
|
|
||||||
function av_brx_id() {
|
|
||||||
if (class_exists('\\Bricks\\Helpers') && method_exists('\\Bricks\\Helpers', 'generate_random_id')) {
|
|
||||||
return \Bricks\Helpers::generate_random_id(false);
|
|
||||||
}
|
|
||||||
return substr(md5(uniqid((string) mt_rand(), true)), 0, 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tiny element factory.
|
|
||||||
*/
|
|
||||||
function av_brx_el($name, $parent, $settings = [], $children = null) {
|
|
||||||
$el = [
|
|
||||||
'id' => av_brx_id(),
|
|
||||||
'name' => $name,
|
|
||||||
'parent' => $parent,
|
|
||||||
'settings' => $settings,
|
|
||||||
];
|
|
||||||
if ($children !== null) {
|
|
||||||
$el['children'] = $children;
|
|
||||||
}
|
|
||||||
return $el;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Industries overview — proof of concept */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_brx_render_industries_overview() {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
|
|
||||||
/* ---- pre-allocate IDs so we can hand them to parents/children ---- */
|
|
||||||
$hero_section_id = av_brx_id();
|
|
||||||
$hero_container_id = av_brx_id();
|
|
||||||
$hero_eyebrow_id = av_brx_id();
|
|
||||||
$hero_h1_id = av_brx_id();
|
|
||||||
$hero_sub_id = av_brx_id();
|
|
||||||
|
|
||||||
$cards_section_id = av_brx_id();
|
|
||||||
$cards_container_id = av_brx_id();
|
|
||||||
$cards_grid_id = av_brx_id();
|
|
||||||
|
|
||||||
$industries = [
|
|
||||||
[
|
|
||||||
'eyebrow' => 'National & Municipal',
|
|
||||||
'title' => 'Police',
|
|
||||||
'excerpt' => 'Vehicle stops, identity checks, urban CQB, de-escalation, riot containment. Built for the realities of contemporary policing.',
|
|
||||||
'href' => '/industries/police/',
|
|
||||||
'cta' => 'Police training',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Tactical units',
|
|
||||||
'title' => 'Special Forces',
|
|
||||||
'excerpt' => 'Hostage rescue, dynamic entries, hostile crowd extraction, sniper-spotter coordination. Built for the units where every error has a name.',
|
|
||||||
'href' => '/industries/special-forces/',
|
|
||||||
'cta' => 'Special forces training',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Armed forces',
|
|
||||||
'title' => 'Military',
|
|
||||||
'excerpt' => 'MOUT, convoy escort, FOB defense, IED awareness, joint operation drills. Built for armed forces that deploy fast and adapt faster.',
|
|
||||||
'href' => '/industries/military/',
|
|
||||||
'cta' => 'Military training',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Emergency services',
|
|
||||||
'title' => 'Firefighters',
|
|
||||||
'excerpt' => "Structure fires, hazmat, multi-casualty triage, confined-space rescue, industrial accidents. Built for first responders who can't rehearse the worst day at full scale.",
|
|
||||||
'href' => '/industries/firefighters/',
|
|
||||||
'cta' => 'Firefighter training',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$card_ids = [];
|
|
||||||
foreach ($industries as $i => $_) {
|
|
||||||
$card_ids[$i] = [
|
|
||||||
'card' => av_brx_id(),
|
|
||||||
'eyebrow' => av_brx_id(),
|
|
||||||
'title' => av_brx_id(),
|
|
||||||
'excerpt' => av_brx_id(),
|
|
||||||
'btn' => av_brx_id(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
$cards_children = array_map(fn($ids) => $ids['card'], $card_ids);
|
|
||||||
|
|
||||||
/* ---- assemble flat element array ---- */
|
|
||||||
$elements = [];
|
|
||||||
|
|
||||||
// 1. Hero section ----------------------------------------------------
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $hero_section_id,
|
|
||||||
'name' => 'section',
|
|
||||||
'parent' => 0,
|
|
||||||
'children' => [$hero_container_id],
|
|
||||||
'settings' => ['_cssClasses' => 'av-hero'],
|
|
||||||
'label' => 'Hero',
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $hero_container_id,
|
|
||||||
'name' => 'container',
|
|
||||||
'parent' => $hero_section_id,
|
|
||||||
'children' => [$hero_eyebrow_id, $hero_h1_id, $hero_sub_id],
|
|
||||||
'settings' => ['_cssClasses' => 'av-container av-hero__inner'],
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $hero_eyebrow_id,
|
|
||||||
'name' => 'text-basic',
|
|
||||||
'parent' => $hero_container_id,
|
|
||||||
'settings' => [
|
|
||||||
'text' => 'Industries',
|
|
||||||
'tag' => 'p',
|
|
||||||
'_cssClasses' => 'av-hero__eyebrow',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $hero_h1_id,
|
|
||||||
'name' => 'heading',
|
|
||||||
'parent' => $hero_container_id,
|
|
||||||
'settings' => [
|
|
||||||
'tag' => 'h1',
|
|
||||||
'text' => 'Built with operators, for operators.',
|
|
||||||
'_cssClasses' => 'av-hero__headline',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $hero_sub_id,
|
|
||||||
'name' => 'text-basic',
|
|
||||||
'parent' => $hero_container_id,
|
|
||||||
'settings' => [
|
|
||||||
'text' => 'PROSERVE™ is not a generic VR platform repackaged for the public sector. Every scenario, every weapon profile, every instructor metric was specified by serving and former operators in close partnership with our engineering team.',
|
|
||||||
'tag' => 'p',
|
|
||||||
'_cssClasses' => 'av-hero__sub',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2. Cards section ---------------------------------------------------
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $cards_section_id,
|
|
||||||
'name' => 'section',
|
|
||||||
'parent' => 0,
|
|
||||||
'children' => [$cards_container_id],
|
|
||||||
'settings' => ['_cssClasses' => 'av-section'],
|
|
||||||
'label' => 'Industries grid',
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $cards_container_id,
|
|
||||||
'name' => 'container',
|
|
||||||
'parent' => $cards_section_id,
|
|
||||||
'children' => [$cards_grid_id],
|
|
||||||
'settings' => ['_cssClasses' => 'av-container'],
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $cards_grid_id,
|
|
||||||
'name' => 'block',
|
|
||||||
'parent' => $cards_container_id,
|
|
||||||
'children' => $cards_children,
|
|
||||||
'settings' => ['_cssClasses' => 'av-grid-4'],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($industries as $i => $ind) {
|
|
||||||
$ids = $card_ids[$i];
|
|
||||||
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $ids['card'],
|
|
||||||
'name' => 'block',
|
|
||||||
'parent' => $cards_grid_id,
|
|
||||||
'children' => [$ids['eyebrow'], $ids['title'], $ids['excerpt'], $ids['btn']],
|
|
||||||
'settings' => ['_cssClasses' => 'av-card'],
|
|
||||||
'label' => $ind['title'],
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $ids['eyebrow'],
|
|
||||||
'name' => 'text-basic',
|
|
||||||
'parent' => $ids['card'],
|
|
||||||
'settings' => [
|
|
||||||
'text' => $ind['eyebrow'],
|
|
||||||
'tag' => 'p',
|
|
||||||
'_cssClasses' => 'av-card__eyebrow',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $ids['title'],
|
|
||||||
'name' => 'heading',
|
|
||||||
'parent' => $ids['card'],
|
|
||||||
'settings' => [
|
|
||||||
'tag' => 'h2',
|
|
||||||
'text' => $ind['title'],
|
|
||||||
'_cssClasses' => 'av-card__title',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $ids['excerpt'],
|
|
||||||
'name' => 'text-basic',
|
|
||||||
'parent' => $ids['card'],
|
|
||||||
'settings' => [
|
|
||||||
'text' => $ind['excerpt'],
|
|
||||||
'tag' => 'p',
|
|
||||||
'_cssClasses' => 'av-card__excerpt',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$elements[] = [
|
|
||||||
'id' => $ids['btn'],
|
|
||||||
'name' => 'button',
|
|
||||||
'parent' => $ids['card'],
|
|
||||||
'settings' => [
|
|
||||||
'text' => $ind['cta'],
|
|
||||||
'tag' => 'a',
|
|
||||||
'link' => [
|
|
||||||
'type' => 'external',
|
|
||||||
'url' => $home_url . ltrim($ind['href'], '/'),
|
|
||||||
],
|
|
||||||
'_cssClasses' => 'av-btn av-btn--secondary av-btn--md',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $elements;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — SEO module
|
|
||||||
*
|
|
||||||
* Hosts custom Schema.org JSON-LD and any meta-tag overrides specific to
|
|
||||||
* Asterion VR. Most generic SEO (title tags, OG, sitemap) is delegated to
|
|
||||||
* Rank Math or Yoast (per BRIEF section 4).
|
|
||||||
*
|
|
||||||
* Planned :
|
|
||||||
* - Organization schema on home + footer-rendered pages
|
|
||||||
* - Product schema on /solutions/* pages (each tier = a Product)
|
|
||||||
* - Article schema on /insights/<slug>/
|
|
||||||
* - FAQPage schema on /trust/ and conversion templates
|
|
||||||
* - BreadcrumbList schema site-wide
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
* @since 0.1.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject Organization JSON-LD on the home page.
|
|
||||||
* Safe minimum — extended once logos/SameAs URLs are confirmed.
|
|
||||||
*/
|
|
||||||
add_action('wp_head', function () {
|
|
||||||
if (! is_front_page()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$org = [
|
|
||||||
'@context' => 'https://schema.org',
|
|
||||||
'@type' => 'Organization',
|
|
||||||
'name' => 'Asterion VR',
|
|
||||||
'url' => home_url('/'),
|
|
||||||
'logo' => get_stylesheet_directory_uri() . '/assets/img/logo-asterion-vr.svg',
|
|
||||||
'foundingDate' => '2016',
|
|
||||||
'address' => [
|
|
||||||
'@type' => 'PostalAddress',
|
|
||||||
'addressLocality' => 'Montgermont',
|
|
||||||
'addressRegion' => 'Bretagne',
|
|
||||||
'addressCountry' => 'FR',
|
|
||||||
],
|
|
||||||
'sameAs' => [
|
|
||||||
// TODO : confirm public profiles with client.
|
|
||||||
// 'https://www.linkedin.com/company/asterion-vr/',
|
|
||||||
// 'https://www.youtube.com/@asterionvr',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
printf(
|
|
||||||
"<script type=\"application/ld+json\">%s</script>\n",
|
|
||||||
wp_json_encode($org, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
|
||||||
);
|
|
||||||
}, 5);
|
|
||||||
|
|
||||||
// TODO : Product schema for /solutions/<tier>/ pages.
|
|
||||||
// TODO : Article schema for /insights/<slug>/ pages.
|
|
||||||
// TODO : FAQPage schema for /trust/ and conversion pages.
|
|
||||||
// TODO : BreadcrumbList schema site-wide.
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Shortcodes
|
|
||||||
*
|
|
||||||
* Lets the migrated post_content reference dynamic blocks (form, recent
|
|
||||||
* posts, etc.) without hardcoding them in the Gutenberg payload.
|
|
||||||
*
|
|
||||||
* [av_form slug="request-demo"] — renders one of the 4 conversion forms
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
add_shortcode('av_form', function ($atts) {
|
|
||||||
$atts = shortcode_atts([
|
|
||||||
'slug' => '',
|
|
||||||
], $atts, 'av_form');
|
|
||||||
|
|
||||||
$slug = sanitize_key($atts['slug']);
|
|
||||||
if (! $slug) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = include AV_THEME_DIR . '/inc/forms-data.php';
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
return '<p style="color:var(--color-error);">[av_form] : unknown slug "' . esc_html($slug) . '"</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
set_query_var('av_form', $data[$slug]);
|
|
||||||
set_query_var('av_form_slug_override', $slug);
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
get_template_part('template-parts/conversion-form');
|
|
||||||
return ob_get_clean();
|
|
||||||
});
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Default index template
|
|
||||||
*
|
|
||||||
* Fallback used by WordPress when no more specific template matches.
|
|
||||||
* Renders standard post lists for archives and the blog hub.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-section">
|
|
||||||
<div class="av-container av-container--narrow">
|
|
||||||
|
|
||||||
<?php if (have_posts()) : ?>
|
|
||||||
<header class="av-section__head">
|
|
||||||
<?php if (is_archive()) : ?>
|
|
||||||
<h1 class="av-section__title"><?php the_archive_title(); ?></h1>
|
|
||||||
<?php $desc = get_the_archive_description(); if ($desc) : ?>
|
|
||||||
<div class="av-section__lead"><?php echo wp_kses_post($desc); ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php elseif (is_search()) : ?>
|
|
||||||
<h1 class="av-section__title"><?php printf(esc_html__('Search results for « %s »', 'asterion-bricks'), get_search_query()); ?></h1>
|
|
||||||
<?php elseif (is_home()) : ?>
|
|
||||||
<h1 class="av-section__title"><?php esc_html_e('Insights', 'asterion-bricks'); ?></h1>
|
|
||||||
<?php endif; ?>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="av-stack av-stack--xl">
|
|
||||||
<?php while (have_posts()) : the_post(); ?>
|
|
||||||
<article <?php post_class('av-card av-card--blog'); ?> style="display:block;background:transparent;box-shadow:none;border-bottom:1px solid var(--color-border-subtle);padding-bottom:var(--space-8);">
|
|
||||||
<header style="display:flex;flex-direction:column;gap:var(--space-2);margin-bottom:var(--space-3);">
|
|
||||||
<span class="av-eyebrow"><?php echo esc_html(get_the_date()); ?></span>
|
|
||||||
<h2 style="font-family:var(--font-display);font-size:var(--text-h2);line-height:var(--leading-h2);font-weight:var(--font-weight-bold);">
|
|
||||||
<a href="<?php the_permalink(); ?>" style="color:inherit;text-decoration:none;"><?php the_title(); ?></a>
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
<div style="color:var(--color-text-secondary);font-size:var(--text-body-md);line-height:var(--leading-body-md);">
|
|
||||||
<?php the_excerpt(); ?>
|
|
||||||
</div>
|
|
||||||
<p style="margin-top:var(--space-3);">
|
|
||||||
<a href="<?php the_permalink(); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Read more', 'asterion-bricks'); ?></a>
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
<?php endwhile; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="av-pagination" style="margin-top:var(--space-12);">
|
|
||||||
<?php the_posts_pagination([
|
|
||||||
'mid_size' => 1,
|
|
||||||
'prev_text' => esc_html__('← Previous', 'asterion-bricks'),
|
|
||||||
'next_text' => esc_html__('Next →', 'asterion-bricks'),
|
|
||||||
]); ?>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<?php else : ?>
|
|
||||||
<header class="av-section__head">
|
|
||||||
<h1 class="av-section__title"><?php esc_html_e('Nothing here yet.', 'asterion-bricks'); ?></h1>
|
|
||||||
<p class="av-section__lead"><?php esc_html_e('No content matches your request. Try a different search, or head back home.', 'asterion-bricks'); ?></p>
|
|
||||||
</header>
|
|
||||||
<p><a class="av-btn av-btn--primary av-btn--md" href="<?php echo esc_url(home_url('/')); ?>"><?php esc_html_e('Back to home', 'asterion-bricks'); ?></a></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Default page template
|
|
||||||
*
|
|
||||||
* If the current page has Bricks data in its `_bricks_page_content_2` post
|
|
||||||
* meta, defer to Bricks' own renderer (Bricks\Frontend::render_content). This
|
|
||||||
* is the same pattern Bricks parent's page.php uses; we keep it here so the
|
|
||||||
* child theme stays in charge of the markup wrapping (header, footer).
|
|
||||||
*
|
|
||||||
* Otherwise fall back to the standard the_content() loop — covers pages
|
|
||||||
* still rendered from post_content (Gutenberg) and any temporarily-empty
|
|
||||||
* page that just hits a non-Bricks Custom Template.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
|
|
||||||
$av_bricks_data = class_exists('\\Bricks\\Helpers')
|
|
||||||
? \Bricks\Helpers::get_bricks_data(get_the_ID(), 'content')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (! empty($av_bricks_data) && class_exists('\\Bricks\\Frontend')) {
|
|
||||||
// Bricks emits its own <main id="brx-content"> wrapper — render with `div`
|
|
||||||
// tag so we don't end up with double <main> (the global one comes from
|
|
||||||
// header.php / footer.php).
|
|
||||||
\Bricks\Frontend::render_content($av_bricks_data, ['id' => 'brx-content'], '', '', 'div');
|
|
||||||
} else {
|
|
||||||
if (have_posts()) {
|
|
||||||
while (have_posts()) {
|
|
||||||
the_post();
|
|
||||||
the_content();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_footer();
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Conversion form partial
|
|
||||||
*
|
|
||||||
* Renders one conversion page (Demo / T&E / Quote / Contact). Two-column
|
|
||||||
* hero (form left, promise right) + optional eligibility list + FAQ.
|
|
||||||
*
|
|
||||||
* Data passed via `set_query_var('av_form', $form_data)`.
|
|
||||||
*
|
|
||||||
* Form posts to admin-post.php with action=av_form_submit; the handler in
|
|
||||||
* inc/form-handler.php validates the nonce, sanitizes, and emails the lead
|
|
||||||
* to jerome.foucher@asterionvr.com.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$form = get_query_var('av_form');
|
|
||||||
if (! is_array($form)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$slug_override = get_query_var('av_form_slug_override');
|
|
||||||
$slug = $slug_override ?: get_post_field('post_name', get_queried_object_id());
|
|
||||||
|
|
||||||
// Surface success/error from query string (set by the handler on redirect).
|
|
||||||
$av_status = isset($_GET['av_status']) ? sanitize_key($_GET['av_status']) : '';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- HERO -->
|
|
||||||
<section class="av-hero" aria-labelledby="form-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 50% 30%, rgba(201,164,90,0.12), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php echo esc_html($form['eyebrow']); ?></p>
|
|
||||||
<h1 id="form-headline" class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);"><?php echo esc_html($form['tagline']); ?></h1>
|
|
||||||
<p class="av-hero__sub"><?php echo esc_html($form['hero_lead']); ?></p>
|
|
||||||
|
|
||||||
<?php if (! empty($form['shortcuts'])) : ?>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<?php foreach ($form['shortcuts'] as $sc) :
|
|
||||||
$variant = ($sc['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--on-dark av-btn--secondary';
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($sc['href'], '/')); ?>" class="av-btn <?php echo esc_attr($variant); ?> av-btn--md">
|
|
||||||
<?php echo esc_html($sc['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- TWO-COLUMN: form (left) + promise (right) -->
|
|
||||||
<section class="av-section" aria-labelledby="form-title">
|
|
||||||
<div class="av-container">
|
|
||||||
|
|
||||||
<?php if ($av_status === 'success') : ?>
|
|
||||||
<div role="status" aria-live="polite" style="padding:var(--space-6);background:rgba(31,139,76,0.08);border-left:3px solid var(--color-success);border-radius:var(--radius-sm);margin-bottom:var(--space-8);">
|
|
||||||
<strong><?php esc_html_e('Thank you — your request was sent.', 'asterion-bricks'); ?></strong><br>
|
|
||||||
<?php esc_html_e('We will reply within the timeframe stated above. Check your inbox (and spam) for confirmation.', 'asterion-bricks'); ?>
|
|
||||||
</div>
|
|
||||||
<?php elseif ($av_status === 'error') : ?>
|
|
||||||
<div role="alert" style="padding:var(--space-6);background:rgba(200,16,46,0.06);border-left:3px solid var(--color-error);border-radius:var(--radius-sm);margin-bottom:var(--space-8);">
|
|
||||||
<strong><?php esc_html_e('Something went wrong.', 'asterion-bricks'); ?></strong>
|
|
||||||
<?php esc_html_e(' Please try again, or email us directly at hello@asterionvr.com.', 'asterion-bricks'); ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-12);" class="av-form-grid">
|
|
||||||
|
|
||||||
<!-- FORM -->
|
|
||||||
<div>
|
|
||||||
<h2 id="form-title" class="av-section__title" style="font-size:var(--text-h2);"><?php esc_html_e('Tell us about your unit.', 'asterion-bricks'); ?></h2>
|
|
||||||
<p class="av-section__lead" style="margin-bottom:var(--space-6);"><?php echo esc_html($form['reassurance']); ?></p>
|
|
||||||
|
|
||||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="av-form">
|
|
||||||
<?php wp_nonce_field('av_form_submit', 'av_nonce'); ?>
|
|
||||||
<input type="hidden" name="action" value="av_form_submit">
|
|
||||||
<input type="hidden" name="form_slug" value="<?php echo esc_attr($slug); ?>">
|
|
||||||
<input type="hidden" name="redirect_to" value="<?php echo esc_attr(get_permalink()); ?>">
|
|
||||||
<input type="text" name="av_hp" value="" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px;" aria-hidden="true">
|
|
||||||
|
|
||||||
<div class="av-stack av-stack--lg">
|
|
||||||
<?php foreach ($form['fields'] as $field) :
|
|
||||||
$field_id = 'av-' . esc_attr($field['name']);
|
|
||||||
$required = ! empty($field['required']);
|
|
||||||
?>
|
|
||||||
<div>
|
|
||||||
<label for="<?php echo esc_attr($field_id); ?>" class="av-label">
|
|
||||||
<?php echo esc_html($field['label']); ?>
|
|
||||||
<?php if ($required) : ?><span class="av-label__required" aria-hidden="true">*</span><?php endif; ?>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<?php if ($field['type'] === 'textarea') : ?>
|
|
||||||
<textarea
|
|
||||||
id="<?php echo esc_attr($field_id); ?>"
|
|
||||||
name="<?php echo esc_attr($field['name']); ?>"
|
|
||||||
class="av-textarea"
|
|
||||||
rows="5"
|
|
||||||
<?php echo $required ? 'required aria-required="true"' : ''; ?>></textarea>
|
|
||||||
|
|
||||||
<?php elseif ($field['type'] === 'select') : ?>
|
|
||||||
<select
|
|
||||||
id="<?php echo esc_attr($field_id); ?>"
|
|
||||||
name="<?php echo esc_attr($field['name']); ?>"
|
|
||||||
class="av-select"
|
|
||||||
<?php echo $required ? 'required aria-required="true"' : ''; ?>>
|
|
||||||
<?php foreach ($field['options'] as $opt) : ?>
|
|
||||||
<option value="<?php echo esc_attr($opt); ?>"<?php echo $opt === '' ? ' disabled selected' : ''; ?>>
|
|
||||||
<?php echo $opt === '' ? esc_html__('— select —', 'asterion-bricks') : esc_html($opt); ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<?php else : ?>
|
|
||||||
<input
|
|
||||||
type="<?php echo esc_attr($field['type']); ?>"
|
|
||||||
id="<?php echo esc_attr($field_id); ?>"
|
|
||||||
name="<?php echo esc_attr($field['name']); ?>"
|
|
||||||
class="av-input"
|
|
||||||
<?php echo $required ? 'required aria-required="true"' : ''; ?>
|
|
||||||
autocomplete="<?php echo $field['type'] === 'email' ? 'email' : 'on'; ?>">
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<button type="submit" class="av-btn av-btn--primary av-btn--lg" style="width:auto;align-self:flex-start;">
|
|
||||||
<?php echo esc_html($form['submit_label'] ?? __('Send', 'asterion-bricks')); ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PROMISE -->
|
|
||||||
<aside style="background:var(--color-bg-subtle);padding:var(--space-8);border-radius:var(--radius-md);align-self:start;">
|
|
||||||
<h2 style="font-family:var(--font-display);font-size:var(--text-h3);font-weight:var(--font-weight-semibold);margin-bottom:var(--space-4);">
|
|
||||||
<?php echo esc_html($form['promise_title']); ?>
|
|
||||||
</h2>
|
|
||||||
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);font-size:var(--text-body-md);line-height:var(--leading-body-md);color:var(--color-text-secondary);">
|
|
||||||
<?php foreach ($form['promise_items'] as $item) : ?>
|
|
||||||
<li style="padding-left:var(--space-6);position:relative;">
|
|
||||||
<span aria-hidden="true" style="position:absolute;left:0;top:0.55em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>
|
|
||||||
<?php echo esc_html($item); ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.av-form-grid { grid-template-columns: 1.4fr 1fr !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ELIGIBILITY (T&E only) -->
|
|
||||||
<?php if (! empty($form['eligibility'])) : ?>
|
|
||||||
<section class="av-section av-section--dark" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('Eligibility', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($form['eligibility']['title']); ?></h2>
|
|
||||||
<p class="av-section__lead"><?php echo esc_html($form['eligibility']['lead']); ?></p>
|
|
||||||
</div>
|
|
||||||
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);max-width:72ch;color:var(--color-text-on-dark-muted);">
|
|
||||||
<?php foreach ($form['eligibility']['items'] as $item) : ?>
|
|
||||||
<li style="padding-left:var(--space-6);position:relative;">
|
|
||||||
<span aria-hidden="true" style="position:absolute;left:0;top:0.6em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>
|
|
||||||
<?php echo esc_html($item); ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- FAQ (Demo only) -->
|
|
||||||
<?php if (! empty($form['faq'])) : ?>
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container av-container--narrow">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Frequently asked', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php esc_html_e('Quick answers.', 'asterion-bricks'); ?></h2>
|
|
||||||
</div>
|
|
||||||
<div class="av-stack av-stack--lg">
|
|
||||||
<?php foreach ($form['faq'] as $i => $qa) : ?>
|
|
||||||
<details style="border-bottom:1px solid var(--color-border-subtle);padding-block:var(--space-4);">
|
|
||||||
<summary style="font-family:var(--font-display);font-size:var(--text-h4);font-weight:var(--font-weight-semibold);cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center;">
|
|
||||||
<?php echo esc_html($qa['q']); ?>
|
|
||||||
<span aria-hidden="true">+</span>
|
|
||||||
</summary>
|
|
||||||
<p style="margin-top:var(--space-3);color:var(--color-text-secondary);font-size:var(--text-body-md);line-height:var(--leading-body-md);">
|
|
||||||
<?php echo esc_html($qa['a']); ?>
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Industry detail partial
|
|
||||||
*
|
|
||||||
* Renders one industry page (Police / Special Forces / Military / Firefighters)
|
|
||||||
* from data passed via `set_query_var('av_industry', $industry_data)`.
|
|
||||||
*
|
|
||||||
* Section types : prose | cards | list | cta (same as solution-detail).
|
|
||||||
*
|
|
||||||
* Hero differs from solution-detail : eyebrow + H1 (tagline) only — no
|
|
||||||
* trademark concat, no upsell block.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$industry = get_query_var('av_industry');
|
|
||||||
if (! is_array($industry)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$home_url = home_url('/');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- HERO -->
|
|
||||||
<section class="av-hero" aria-labelledby="industry-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 50% 40%, rgba(11,31,58,0.4), transparent 65%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #1F3252 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php echo esc_html($industry['eyebrow']); ?></p>
|
|
||||||
<h1 id="industry-headline" class="av-hero__headline"><?php echo esc_html($industry['tagline']); ?></h1>
|
|
||||||
<p class="av-hero__sub"><?php echo esc_html($industry['hero_lead']); ?></p>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<?php foreach ($industry['ctas'] as $cta) :
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary av-btn--on-dark';
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($cta['href'], '/')); ?>" class="av-btn <?php echo esc_attr($variant); ?> av-btn--lg">
|
|
||||||
<?php echo esc_html($cta['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- SECTIONS -->
|
|
||||||
<?php
|
|
||||||
foreach ($industry['sections'] as $idx => $section) :
|
|
||||||
$is_dark = ($idx % 2 === 1);
|
|
||||||
$section_class = 'av-section' . ($is_dark ? ' av-section--dark' : '');
|
|
||||||
?>
|
|
||||||
<section class="<?php echo esc_attr($section_class); ?>" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow<?php echo $is_dark ? ' av-eyebrow--gold' : ''; ?>"><?php echo esc_html($section['eyebrow']); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($section['title']); ?></h2>
|
|
||||||
<?php if (! empty($section['lead'])) : ?>
|
|
||||||
<p class="av-section__lead"><?php echo esc_html($section['lead']); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($section['type'] === 'prose' && ! empty($section['body'])) : ?>
|
|
||||||
<p class="av-section__lead" style="max-width:72ch;font-size:var(--text-body-lg);">
|
|
||||||
<?php echo esc_html($section['body']); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'cards' && ! empty($section['items'])) : ?>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($section['items'] as $card) : ?>
|
|
||||||
<article class="av-card<?php echo $is_dark ? ' is-on-dark' : ''; ?> av-card--feature">
|
|
||||||
<div class="av-card__body">
|
|
||||||
<h3 class="av-card__title" style="font-size:var(--text-h4);"><?php echo esc_html($card['title']); ?></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($card['body']); ?></p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'list' && ! empty($section['items'])) : ?>
|
|
||||||
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);max-width:72ch;font-size:var(--text-body-lg);line-height:var(--leading-body-lg);<?php echo $is_dark ? 'color:var(--color-text-on-dark-muted);' : 'color:var(--color-text-secondary);'; ?>">
|
|
||||||
<?php foreach ($section['items'] as $item) : ?>
|
|
||||||
<li style="padding-left:var(--space-6);position:relative;">
|
|
||||||
<span aria-hidden="true" style="position:absolute;left:0;top:0.6em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>
|
|
||||||
<?php echo esc_html($item); ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php if (! empty($section['cta'])) : ?>
|
|
||||||
<p style="margin-top:var(--space-8);">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($section['cta']['href'], '/')); ?>" class="av-link<?php echo $is_dark ? ' av-link--on-dark' : ''; ?> av-link--with-arrow">
|
|
||||||
<?php echo esc_html($section['cta']['label']); ?>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<!-- CONVERSION BANNER (shared) -->
|
|
||||||
<section class="av-conversion-banner">
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-conversion-banner__head">
|
|
||||||
<h2 class="av-conversion-banner__title">Train like the threat is now.</h2>
|
|
||||||
<p class="av-conversion-banner__lead">Three paths into the platform — pick the one that fits where you are.</p>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__grid">
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a Demo</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A 30-minute tailored session with our team.</p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--md">Schedule a demo</a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a T&E Kit</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A PROSERVE case on loan at your facility for 5 to 10 days.</p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-te-kit/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md">Request the kit</a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a Quote</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A detailed quote and deployment plan within 5 working days.</p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-quote/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md">Get a quote</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Solution detail partial
|
|
||||||
*
|
|
||||||
* Renders one PROSERVE tier (MY / FLEX / ACADEMY / +) from data passed via
|
|
||||||
* `set_query_var('av_solution', $solution_data)` before include.
|
|
||||||
*
|
|
||||||
* Section types supported : prose | cards | list | cta
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$solution = get_query_var('av_solution');
|
|
||||||
if (! is_array($solution)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$home_url = home_url('/');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- HERO -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-hero" aria-labelledby="solution-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 70% 30%, rgba(201,164,90,0.15), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php echo esc_html($solution['eyebrow']); ?></p>
|
|
||||||
<h1 id="solution-headline" class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);">
|
|
||||||
<?php echo esc_html($solution['title']); ?>™ — <?php echo esc_html($solution['tagline']); ?>
|
|
||||||
</h1>
|
|
||||||
<p class="av-hero__sub"><?php echo esc_html($solution['hero_lead']); ?></p>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<?php foreach ($solution['ctas'] as $i => $cta) :
|
|
||||||
$on_dark = ($cta['variant'] === 'secondary') ? ' av-btn--on-dark' : '';
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary';
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($cta['href'], '/')); ?>" class="av-btn <?php echo esc_attr($variant); ?><?php echo esc_attr($on_dark); ?> av-btn--lg">
|
|
||||||
<?php echo esc_html($cta['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- SECTIONS -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<?php
|
|
||||||
foreach ($solution['sections'] as $idx => $section) :
|
|
||||||
$is_dark = ($idx % 2 === 1);
|
|
||||||
$section_class = 'av-section' . ($is_dark ? ' av-section--dark' : '');
|
|
||||||
?>
|
|
||||||
<section class="<?php echo esc_attr($section_class); ?>" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow<?php echo $is_dark ? ' av-eyebrow--gold' : ''; ?>"><?php echo esc_html($section['eyebrow']); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($section['title']); ?></h2>
|
|
||||||
<?php if (! empty($section['lead'])) : ?>
|
|
||||||
<p class="av-section__lead"><?php echo esc_html($section['lead']); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($section['type'] === 'prose' && ! empty($section['body'])) : ?>
|
|
||||||
<p class="av-section__lead" style="max-width:72ch;font-size:var(--text-body-lg);">
|
|
||||||
<?php echo esc_html($section['body']); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'cards' && ! empty($section['items'])) : ?>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($section['items'] as $card) : ?>
|
|
||||||
<article class="av-card<?php echo $is_dark ? ' is-on-dark' : ''; ?> av-card--feature">
|
|
||||||
<div class="av-card__body">
|
|
||||||
<h3 class="av-card__title" style="font-size:var(--text-h4);"><?php echo esc_html($card['title']); ?></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($card['body']); ?></p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'list' && ! empty($section['items'])) : ?>
|
|
||||||
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);max-width:72ch;font-size:var(--text-body-lg);line-height:var(--leading-body-lg);<?php echo $is_dark ? 'color:var(--color-text-on-dark-muted);' : 'color:var(--color-text-secondary);'; ?>">
|
|
||||||
<?php foreach ($section['items'] as $item) : ?>
|
|
||||||
<li style="padding-left:var(--space-6);position:relative;">
|
|
||||||
<span aria-hidden="true" style="position:absolute;left:0;top:0.6em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>
|
|
||||||
<?php echo esc_html($item); ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php if (! empty($section['cta'])) : ?>
|
|
||||||
<p style="margin-top:var(--space-8);">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($section['cta']['href'], '/')); ?>" class="av-link<?php echo $is_dark ? ' av-link--on-dark' : ''; ?> av-link--with-arrow">
|
|
||||||
<?php echo esc_html($section['cta']['label']); ?>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'cta' && ! empty($section['body'])) : ?>
|
|
||||||
<p class="av-section__lead" style="max-width:72ch;margin-bottom:var(--space-6);"><?php echo esc_html($section['body']); ?></p>
|
|
||||||
<?php if (! empty($section['ctas'])) : ?>
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-4);">
|
|
||||||
<?php foreach ($section['ctas'] as $cta) :
|
|
||||||
$on_dark = $is_dark ? ' av-btn--on-dark' : '';
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary';
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($cta['href'], '/')); ?>" class="av-btn <?php echo esc_attr($variant); ?><?php echo esc_attr($on_dark); ?> av-btn--lg">
|
|
||||||
<?php echo esc_html($cta['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- UPSELL (when to step up to the next tier) -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<?php if (! empty($solution['upsell'])) : ?>
|
|
||||||
<section class="av-section" style="background-color:var(--color-bg-subtle);" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Comparison and upgrade', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($solution['upsell']['title']); ?></h2>
|
|
||||||
<p class="av-section__lead"><?php echo esc_html($solution['upsell']['body']); ?></p>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-4);">
|
|
||||||
<?php foreach ($solution['upsell']['links'] as $link) : ?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($link['href'], '/')); ?>" class="av-btn av-btn--secondary av-btn--md">
|
|
||||||
<?php echo esc_html($link['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- CONVERSION BANNER (shared) -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-conversion-banner">
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-conversion-banner__head">
|
|
||||||
<h2 class="av-conversion-banner__title"><?php esc_html_e('Ready to get hands-on?', 'asterion-bricks'); ?></h2>
|
|
||||||
<p class="av-conversion-banner__lead"><?php esc_html_e('Three paths into the platform — pick the one that fits where you are.', 'asterion-bricks'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__grid">
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a Demo', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('A 30-minute tailored session with our team. Walk through the configuration that matches your needs.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--md"><?php esc_html_e('Schedule a demo', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a T&E Kit', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('A PROSERVE case on loan at your facility for 5 to 10 days, with on-site support from our team.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-te-kit/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md"><?php esc_html_e('Request the kit', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a Quote', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('A detailed quote and deployment plan within 5 working days.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-quote/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md"><?php esc_html_e('Get a quote', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Conversion Form
|
|
||||||
*
|
|
||||||
* Custom page template for /request-demo/, /request-te-kit/, /request-quote/, /contact/.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('forms-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Form data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
set_query_var('av_form', $data[$slug]);
|
|
||||||
get_template_part('template-parts/conversion-form');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Editorial Detail
|
|
||||||
*
|
|
||||||
* Custom page template for editorial pages (Why Asterion, About, Customers
|
|
||||||
* hub, Trust, Partners). Reuses the industry-detail partial.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('editorial-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Editorial data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
set_query_var('av_industry', $data[$slug]);
|
|
||||||
get_template_part('template-parts/industry-detail');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Industries Overview
|
|
||||||
*
|
|
||||||
* Hub page /industries/ — lists the 4 industries as cards.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
|
|
||||||
$industries = [
|
|
||||||
[
|
|
||||||
'eyebrow' => 'National & Municipal',
|
|
||||||
'title' => 'Police',
|
|
||||||
'excerpt' => 'Vehicle stops, identity checks, urban CQB, de-escalation, riot containment. Built for the realities of contemporary policing.',
|
|
||||||
'href' => '/industries/police/',
|
|
||||||
'gradient' => 'linear-gradient(135deg,#1F3252,#0B1F3A)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Tactical units',
|
|
||||||
'title' => 'Special Forces',
|
|
||||||
'excerpt' => 'Hostage rescue, dynamic entries, hostile crowd extraction, sniper-spotter coordination. Built for the units where every error has a name.',
|
|
||||||
'href' => '/industries/special-forces/',
|
|
||||||
'gradient' => 'linear-gradient(135deg,#1F3252,#0B1F3A)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Armed forces',
|
|
||||||
'title' => 'Military',
|
|
||||||
'excerpt' => 'MOUT, convoy escort, FOB defense, IED awareness, joint operation drills. Built for armed forces that deploy fast and adapt faster.',
|
|
||||||
'href' => '/industries/military/',
|
|
||||||
'gradient' => 'linear-gradient(135deg,#1F3252,#0B1F3A)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Emergency services',
|
|
||||||
'title' => 'Firefighters',
|
|
||||||
'excerpt' => "Structure fires, hazmat, multi-casualty triage, confined-space rescue, industrial accidents. Built for first responders who can't rehearse the worst day at full scale.",
|
|
||||||
'href' => '/industries/firefighters/',
|
|
||||||
'gradient' => 'linear-gradient(135deg,#1F3252,#0B1F3A)',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-hero" aria-labelledby="industries-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 50% 40%, rgba(11,31,58,0.4), transparent 65%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #1F3252 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Industries', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="industries-headline" class="av-hero__headline">Built with operators, for operators.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
PROSERVE™ is not a generic VR platform repackaged for the public sector. Every scenario, every weapon profile, every instructor metric was specified by serving and former operators in close partnership with our engineering team.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-grid-4" style="grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));">
|
|
||||||
<?php foreach ($industries as $i) : ?>
|
|
||||||
<article class="av-card">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($i['href'], '/')); ?>" class="av-card__media" aria-hidden="true" tabindex="-1" style="background:<?php echo esc_attr($i['gradient']); ?>;display:block;"></a>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php echo esc_html($i['eyebrow']); ?></span>
|
|
||||||
<h2 class="av-card__title"><a href="<?php echo esc_url($home_url . ltrim($i['href'], '/')); ?>" style="color:inherit;text-decoration:none;"><?php echo esc_html($i['title']); ?></a></h2>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($i['excerpt']); ?></p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($i['href'], '/')); ?>" class="av-link av-link--with-arrow"><?php echo esc_html($i['title']); ?> training</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Industry Detail
|
|
||||||
*
|
|
||||||
* Custom page template for /industries/police/, /industries/special-forces/,
|
|
||||||
* /industries/military/, /industries/firefighters/.
|
|
||||||
*
|
|
||||||
* Looks up the page slug in inc/industries-data.php and hands the matched
|
|
||||||
* tier off to template-parts/industry-detail.php.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('industries-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Industry data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
set_query_var('av_industry', $data[$slug]);
|
|
||||||
get_template_part('template-parts/industry-detail');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Insights Hub
|
|
||||||
*
|
|
||||||
* /insights/ — blog hub. Lists native `post` items as blog cards.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-hero" aria-labelledby="insights-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 60% 40%, rgba(201,164,90,0.12), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 70%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Insights', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="insights-headline" class="av-hero__headline">Notes from the front of immersive training.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
Doctrine. Training science. Instructor stories. AAR data. Comparisons of VR vs live training. Found something useful? It probably came from a customer conversation. We publish what we learn.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<?php
|
|
||||||
// Categories filter chips (placeholders — wired to native `category` taxonomy if used).
|
|
||||||
$categories = ['Doctrine', 'Technology', 'Case Studies', 'Industry News', "Founder's Notes"];
|
|
||||||
?>
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-3);margin-bottom:var(--space-10);">
|
|
||||||
<?php foreach ($categories as $cat) : ?>
|
|
||||||
<span class="av-btn av-btn--ghost av-btn--sm" style="border:1px solid var(--color-border-default);"><?php echo esc_html($cat); ?></span>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php $posts = get_posts(['numberposts' => 12, 'post_status' => 'publish']); ?>
|
|
||||||
<?php if (! empty($posts)) : ?>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($posts as $post) : setup_postdata($post); ?>
|
|
||||||
<article class="av-card av-card--blog">
|
|
||||||
<a href="<?php the_permalink(); ?>" class="av-card__media" aria-hidden="true" tabindex="-1" style="background:linear-gradient(135deg,#CCD3DC,#E5E9EE);"></a>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php echo esc_html(get_the_date('M j, Y')); ?></span>
|
|
||||||
<h2 class="av-card__title"><a href="<?php the_permalink(); ?>" style="color:inherit;text-decoration:none;"><?php the_title(); ?></a></h2>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html(wp_trim_words(get_the_excerpt(), 22)); ?></p>
|
|
||||||
<div class="av-card__cta"><a href="<?php the_permalink(); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Read more', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; wp_reset_postdata(); ?>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<p style="color:var(--color-text-muted);max-width:64ch;">
|
|
||||||
<?php esc_html_e('Editorial blog launches with the site go-live. The 12-month content calendar is in the Strategy & Content brief, section B.4. New articles will appear here as soon as they ship.', 'asterion-bricks'); ?>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="av-section av-section--dark" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-8);align-items:center;" class="av-newsletter-row">
|
|
||||||
<div>
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('Newsletter', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 style="font-family:var(--font-display);font-size:var(--text-h1);line-height:var(--leading-h1);color:var(--color-text-on-dark);margin:var(--space-3) 0;">Subscribe — one article per month.</h2>
|
|
||||||
<p style="color:var(--color-text-on-dark-muted);max-width:60ch;">
|
|
||||||
Insights from the front of immersive training. One email per month. No spam. Unsubscribe in one click.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<form method="post" action="#" style="display:flex;gap:var(--space-3);flex-wrap:wrap;align-items:flex-start;" aria-label="<?php esc_attr_e('Newsletter signup', 'asterion-bricks'); ?>">
|
|
||||||
<label for="av-insights-newsletter" class="av-sr-only"><?php esc_html_e('Your email', 'asterion-bricks'); ?></label>
|
|
||||||
<input type="email" id="av-insights-newsletter" name="email" class="av-input" placeholder="<?php esc_attr_e('your@email.com', 'asterion-bricks'); ?>" required style="flex:1;min-width:200px;">
|
|
||||||
<button type="submit" class="av-btn av-btn--primary av-btn--md"><?php esc_html_e('Subscribe', 'asterion-bricks'); ?></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<style>@media (min-width: 768px) { .av-newsletter-row { grid-template-columns: 2fr 1.5fr !important; } }</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Legal
|
|
||||||
*
|
|
||||||
* /legal/privacy/, /legal/terms/, /legal/cookies/, /legal/notice/.
|
|
||||||
* Renders the WP page content (filled in by counsel) inside the standard
|
|
||||||
* narrow container with a clean editorial hero.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-section">
|
|
||||||
<div class="av-container av-container--narrow">
|
|
||||||
|
|
||||||
<header class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Legal', 'asterion-bricks'); ?></span>
|
|
||||||
<h1 class="av-section__title"><?php the_title(); ?></h1>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
<?php esc_html_e('Last updated:', 'asterion-bricks'); ?>
|
|
||||||
<?php echo esc_html(get_the_modified_date()); ?>
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="av-prose" style="font-size:var(--text-body-md);line-height:var(--leading-body-md);color:var(--color-text-secondary);">
|
|
||||||
<?php
|
|
||||||
if (have_posts()) :
|
|
||||||
while (have_posts()) : the_post();
|
|
||||||
the_content();
|
|
||||||
endwhile;
|
|
||||||
endif;
|
|
||||||
|
|
||||||
// Default skeleton when the page content is empty — populated by counsel later.
|
|
||||||
$content = get_post_field('post_content', get_queried_object_id());
|
|
||||||
if (trim($content) === '') :
|
|
||||||
?>
|
|
||||||
<p style="padding:var(--space-6);background:var(--color-bg-subtle);border-left:3px solid var(--color-warning);border-radius:var(--radius-sm);">
|
|
||||||
<strong><?php esc_html_e('Placeholder.', 'asterion-bricks'); ?></strong>
|
|
||||||
<?php esc_html_e('This page is to be drafted with legal counsel. The structure below outlines the sections expected.', 'asterion-bricks'); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Slug-driven placeholder sections.
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$skeletons = [
|
|
||||||
'privacy' => [
|
|
||||||
'Data controller (Asterion VR), contact, DPO if relevant',
|
|
||||||
'Data collected (form submissions, cookie identifiers, IP, etc.)',
|
|
||||||
'Purpose of processing (sales follow-up, newsletter, analytics)',
|
|
||||||
'Legal basis (consent, contract, legitimate interest)',
|
|
||||||
'Retention periods',
|
|
||||||
'Data sharing (subcontractors, hosts)',
|
|
||||||
'Data subject rights (access, rectification, deletion, portability, objection)',
|
|
||||||
'Complaint procedure (CNIL link)',
|
|
||||||
],
|
|
||||||
'terms' => [
|
|
||||||
'Site usage conditions',
|
|
||||||
'Intellectual property',
|
|
||||||
'Liability limitations',
|
|
||||||
'Applicable law and jurisdiction',
|
|
||||||
],
|
|
||||||
'cookies' => [
|
|
||||||
'List of cookies (functional, analytics, marketing)',
|
|
||||||
'Cookie purpose, retention, third parties',
|
|
||||||
'Consent management procedure',
|
|
||||||
],
|
|
||||||
'notice' => [
|
|
||||||
'Asterion VR — RCS 821 520 970 Rennes — NAF 6201Z — SIRET 821 520 970 00012',
|
|
||||||
'Adresse, téléphone, e-mail, directeur de la publication',
|
|
||||||
'Hébergeur',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isset($skeletons[$slug])) :
|
|
||||||
?>
|
|
||||||
<h2 style="font-family:var(--font-display);font-size:var(--text-h3);margin-top:var(--space-10);margin-bottom:var(--space-4);"><?php esc_html_e('Sections to include', 'asterion-bricks'); ?></h2>
|
|
||||||
<ul style="display:flex;flex-direction:column;gap:var(--space-2);padding-left:var(--space-6);">
|
|
||||||
<?php foreach ($skeletons[$slug] as $line) : ?>
|
|
||||||
<li style="list-style:disc;"><?php echo esc_html($line); ?></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Resources
|
|
||||||
*
|
|
||||||
* /resources/ — datasheets, whitepapers, videos, brochures.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
|
|
||||||
$groups = [
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Datasheets',
|
|
||||||
'title' => 'Technical specifications.',
|
|
||||||
'items' => [
|
|
||||||
['title' => 'PROSERVE — full datasheet', 'note' => 'Hardware specifications, software capabilities, scenarios. Gated, light form.'],
|
|
||||||
['title' => 'MY PROSERVE — quick datasheet', 'note' => '4-page summary.'],
|
|
||||||
['title' => 'PROSERVE FLEX — quick datasheet', 'note' => '4-page summary.'],
|
|
||||||
['title' => 'PROSERVE ACADEMY — quick datasheet', 'note' => '4-page summary.'],
|
|
||||||
['title' => 'PROSERVE+ — customization brochure', 'note' => 'Hardware add-ons, custom scenarios, process.'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Whitepapers',
|
|
||||||
'title' => 'Long-form analyses.',
|
|
||||||
'items' => [
|
|
||||||
['title' => 'VR vs Live Fire: the 2026 cost-benefit comparison', 'note' => 'Quantified comparison across cost, fidelity, scalability, environmental.'],
|
|
||||||
['title' => "The Instructor's Guide to VR Training Programs", 'note' => "Methodology, drill design, AAR debriefing, assessment frameworks."],
|
|
||||||
['title' => 'Use of Force Training: how immersive simulation reduces tactical errors', 'note' => 'Reviews academic literature and Asterion deployment data.'],
|
|
||||||
['title' => 'Sovereignty in security simulation: why your training data must never leave your network', 'note' => 'For procurement and IT teams.'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Videos',
|
|
||||||
'title' => 'Show, not tell.',
|
|
||||||
'items' => [
|
|
||||||
['title' => 'PROSERVE — 90-second product film', 'note' => 'Cinematic overview.'],
|
|
||||||
['title' => 'Instructor Cockpit walkthrough (3 minutes)', 'note' => 'Inside the instructor PC.'],
|
|
||||||
['title' => 'AAR replay — annotated example (4 minutes)', 'note' => 'A drill replayed with overlays.'],
|
|
||||||
['title' => 'MY PROSERVE — unboxing & first deployment (6 min)', 'note' => 'From mini-case to first scenario in real time.'],
|
|
||||||
['title' => 'Customer story — [name redacted unit] (8 min)', 'note' => 'Operational deployment, instructor interview, AAR excerpts.'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Brochures',
|
|
||||||
'title' => 'For procurement and end-user briefs.',
|
|
||||||
'items' => [
|
|
||||||
['title' => 'PROSERVE general brochure', 'note' => '8 pages, EN/FR.'],
|
|
||||||
['title' => 'Industry-specific brochures', 'note' => 'Police, Special Forces, Military, Firefighters — one PDF each.'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-hero" aria-labelledby="resources-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Resources', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="resources-headline" class="av-hero__headline">Everything to evaluate, justify, and procure.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
Whether you are building an internal pitch, drafting a procurement specification, or briefing your team — these resources are designed to help.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php foreach ($groups as $idx => $group) :
|
|
||||||
$is_dark = ($idx % 2 === 1);
|
|
||||||
$section_class = 'av-section' . ($is_dark ? ' av-section--dark' : '');
|
|
||||||
?>
|
|
||||||
<section class="<?php echo esc_attr($section_class); ?>" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow<?php echo $is_dark ? ' av-eyebrow--gold' : ''; ?>"><?php echo esc_html($group['eyebrow']); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($group['title']); ?></h2>
|
|
||||||
</div>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($group['items'] as $item) : ?>
|
|
||||||
<article class="av-card<?php echo $is_dark ? ' is-on-dark' : ''; ?>">
|
|
||||||
<div class="av-card__body">
|
|
||||||
<h3 class="av-card__title" style="font-size:var(--text-h4);"><?php echo esc_html($item['title']); ?></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($item['note']); ?></p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'contact/'); ?>" class="av-link<?php echo $is_dark ? ' av-link--on-dark' : ''; ?> av-link--with-arrow"><?php esc_html_e('Request access', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Solution Detail
|
|
||||||
*
|
|
||||||
* Custom page template assigned to /solutions/my-proserve/, /solutions/proserve-flex/,
|
|
||||||
* /solutions/proserve-academy/ and /solutions/customization/.
|
|
||||||
*
|
|
||||||
* Looks up the page slug in inc/solutions-data.php, hands the matched array off
|
|
||||||
* to template-parts/solution-detail.php, and renders the result wrapped in the
|
|
||||||
* standard header + footer.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('solutions-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
// Fallback : fall back to the basic page rendering if the slug isn't mapped.
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Solution data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
set_query_var('av_solution', $data[$slug]);
|
|
||||||
get_template_part('template-parts/solution-detail');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Solutions Overview
|
|
||||||
*
|
|
||||||
* Custom page template assigned to /solutions/. Renders the 4-tier comparison
|
|
||||||
* and PROSERVE+ teaser per PDF Strategie & Contenu D.2.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- HERO -->
|
|
||||||
<section class="av-hero" aria-labelledby="solutions-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 30% 30%, rgba(201,164,90,0.15), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Solutions', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="solutions-headline" class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);">
|
|
||||||
One platform. Three formats. Zero rip-and-replace.
|
|
||||||
</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
Whether you train one officer or sixteen, on a single mission or a full curriculum, PROSERVE™ delivers the same software, the same scenarios, the same instructor tools — at the right scale for your operation. Start solo. Add operators. Scale to a full academy. Your investment never resets.
|
|
||||||
</p>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--lg"><?php esc_html_e('Request a Demo', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- COMPARISON TABLE -->
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Compare', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title">Compare the three solutions.</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
Same software. Same scenarios. Same instructor tools. Different format, different scale.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="overflow-x:auto;border:1px solid var(--color-border-subtle);border-radius:var(--radius-md);">
|
|
||||||
<table style="width:100%;border-collapse:collapse;font-size:var(--text-body-md);min-width:720px;">
|
|
||||||
<thead>
|
|
||||||
<tr style="background:var(--color-brand-navy);color:var(--color-text-on-dark);">
|
|
||||||
<th scope="col" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);"></th>
|
|
||||||
<th scope="col" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);">MY PROSERVE</th>
|
|
||||||
<th scope="col" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);">PROSERVE FLEX</th>
|
|
||||||
<th scope="col" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);">PROSERVE ACADEMY</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php
|
|
||||||
$rows = [
|
|
||||||
['Trainees', '1 (solo)', '1–5 (team)', '1–16 (unit)'],
|
|
||||||
['Format', 'Mini-case', 'One case per operator + instructor PC', 'Dedicated room + rack + instructor station'],
|
|
||||||
['Floor space', '3 m² minimum', '9 m² to 100 m²', 'Up to 900 m²'],
|
|
||||||
['Setup time', '< 5 min', '< 5 min (solo) / 20 min (team)', 'Pre-installed (rapid recalibration)'],
|
|
||||||
['Multi-user cooperative', 'No', 'Yes (up to 5)', 'Yes (up to 16)'],
|
|
||||||
['Mobility', 'Ultra-portable', 'Highly portable', 'Fixed'],
|
|
||||||
['Best for', 'Individual continuous training (police municipale, on-call officers)', 'Tactical teams, mobile units, special operations', 'Academies, central training centres, large units'],
|
|
||||||
['Indicative price', 'From €9,000 / unit', 'On request', 'On request'],
|
|
||||||
];
|
|
||||||
foreach ($rows as $i => $row) :
|
|
||||||
$bg = ($i % 2 === 0) ? 'transparent' : 'var(--color-bg-subtle)';
|
|
||||||
?>
|
|
||||||
<tr style="background:<?php echo esc_attr($bg); ?>;">
|
|
||||||
<th scope="row" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);color:var(--color-text-primary);border-bottom:1px solid var(--color-border-subtle);">
|
|
||||||
<?php echo esc_html($row[0]); ?>
|
|
||||||
</th>
|
|
||||||
<?php for ($c = 1; $c <= 3; $c++) : ?>
|
|
||||||
<td style="padding:var(--space-4);color:var(--color-text-secondary);border-bottom:1px solid var(--color-border-subtle);">
|
|
||||||
<?php echo esc_html($row[$c]); ?>
|
|
||||||
</td>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-4);margin-top:var(--space-8);">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/my-proserve/'); ?>" class="av-btn av-btn--secondary av-btn--md">MY PROSERVE</a>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/proserve-flex/'); ?>" class="av-btn av-btn--secondary av-btn--md">PROSERVE FLEX</a>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/proserve-academy/'); ?>" class="av-btn av-btn--secondary av-btn--md">PROSERVE ACADEMY</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- PROSERVE+ TEASER -->
|
|
||||||
<section class="av-section av-section--dark" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-10);align-items:center;" class="av-plus-row">
|
|
||||||
<div style="aspect-ratio:4/3;background:linear-gradient(135deg,#13294B,#081427);border-radius:var(--radius-md);"></div>
|
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-4);">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('PROSERVE+', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title">Need something the catalog doesn’t list?</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
PROSERVE+™ is the customization track. Wrist trackers, haptic vests, custom weapon replicas, bespoke environments, scenarios in your unit’s exact uniforms and doctrine. Designed and produced in our French studio.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/customization/'); ?>" class="av-btn av-btn--primary av-btn--md">
|
|
||||||
<?php esc_html_e('Explore PROSERVE+ customization', 'asterion-bricks'); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<style>@media (min-width: 1024px) { .av-plus-row { grid-template-columns: 1fr 1.2fr !important; } }</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Technology Detail
|
|
||||||
*
|
|
||||||
* Custom page template for the 6 /technology/* sub-pages.
|
|
||||||
* Same data schema as industries — reuses template-parts/industry-detail.php.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('technology-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Technology data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
// industry-detail partial expects the data under `av_industry`.
|
|
||||||
set_query_var('av_industry', $data[$slug]);
|
|
||||||
get_template_part('template-parts/industry-detail');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Technology Overview
|
|
||||||
*
|
|
||||||
* Hub /technology/ — six pillars per PDF Strategie & Contenu D.11.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
|
|
||||||
$pillars = [
|
|
||||||
[
|
|
||||||
'eyebrow' => '01',
|
|
||||||
'title' => 'VR Hardware',
|
|
||||||
'body' => 'Fully wireless 5K headset. Wi-Fi 6E streaming from a dedicated rendering laptop. 90 Hz refresh, 120° field of view, sub-millimeter inside-out tracking. Designed for daily operational use.',
|
|
||||||
'cta' => 'Inside the hardware',
|
|
||||||
'href' => '/technology/vr-hardware/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '02',
|
|
||||||
'title' => 'Software & AI',
|
|
||||||
'body' => 'Powered by Unreal Engine 5. Autonomous AI agents with vision, hearing, and personality. Real-time ballistic simulation. Locally hosted, no cloud dependency.',
|
|
||||||
'cta' => 'Inside the software',
|
|
||||||
'href' => '/technology/software-ai/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '03',
|
|
||||||
'title' => 'Weapons & Tracking',
|
|
||||||
'body' => 'Universal Weapon Digitizer (UWeD) attaches to gas-blowback replicas, CO2 replicas, or real service weapons firing blanks. Recoil-induced tracking loss is corrected by an exclusive IMU compensation system.',
|
|
||||||
'cta' => 'Inside weapons & tracking',
|
|
||||||
'href' => '/technology/weapons-tracking/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '04',
|
|
||||||
'title' => 'Scenarios Library',
|
|
||||||
'body' => 'Eleven environments. More than fifty validated scenarios. Updates four times per year. From fire range and IPSC to multi-room CQB, supermarket, residential apartment, and industrial sites.',
|
|
||||||
'cta' => 'Browse the scenario library',
|
|
||||||
'href' => '/technology/scenarios/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '05',
|
|
||||||
'title' => 'Instructor Cockpit',
|
|
||||||
'body' => "Real-time supervision from any angle. Walk through walls. Switch into any trainee's view. Toggle shot trace, field of view, threat highlighting. Manipulate the scenario in real time.",
|
|
||||||
'cta' => 'Inside the instructor cockpit',
|
|
||||||
'href' => '/technology/instructor-cockpit/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '06',
|
|
||||||
'title' => 'After-Action Review',
|
|
||||||
'body' => 'Frame-level replay from any camera angle. Per-trainee statistics. Per-unit dashboards. Comparable across sessions. Exportable for command review.',
|
|
||||||
'cta' => 'Inside after-action review',
|
|
||||||
'href' => '/technology/after-action-review/',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-hero" aria-labelledby="technology-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 50% 30%, rgba(201,164,90,0.18), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Technology', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="technology-headline" class="av-hero__headline">Engineered for the unforgiving.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
Six layers of technology, each engineered against a real failure mode of conventional simulators. Wireless 5K visuals. Real-recoil weapons. Autonomous AI agents. Ballistic physics. An instructor cockpit that sees through walls. And an after-action review that ends every session with data.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('The six pillars', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title">The six pillars of PROSERVE™.</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($pillars as $p) : ?>
|
|
||||||
<article class="av-card av-card--feature">
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow" style="color:var(--color-brand-gold);"><?php echo esc_html($p['eyebrow']); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php echo esc_html($p['title']); ?></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($p['body']); ?></p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($p['href'], '/')); ?>" class="av-link av-link--with-arrow"><?php echo esc_html($p['cta']); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
Reference in New Issue
Block a user