Compare commits

..

10 Commits

Author SHA1 Message Date
918c000817 feat(insights): blog setup — hub page + Single Post template + 3 seed posts
Phase 12 (post-launch ready) — Insights blog scaffolding so the /insights/
menu link works and the user can start publishing.

Creates :
1. /insights/ hub page (#153) with hero + Bricks `posts` query-loop
   element + closer CTA. The posts element configured with :
   - query : post_type=post, posts_per_page=9, orderby=date DESC
   - layout=grid, columns=3, linkPost=true, imageDisable=true
   - fields : eyebrow "Insights", h3 post title, date, excerpt
   - _cssCustom for card styling (subtle bg, border, padding, flex column)
2. Single Post Bricks template (#154, template_type=single, conditions=
   singular post) : eyebrow + h1 {post_title} + date · author meta +
   featured image + post-content + closer CTA.
3. Three seed posts publication-ready :
   - the-10-to-15-gap (#155)
   - simulators-for-entertainment-vs-training (#156)
   - sovereign-by-default-gdpr-native (#157)
   Topics aligned with the brand : training gap reality, why
   entertainment-first simulators fail tactical training, GDPR-native
   sovereignty for security forces.

User adds more posts via WP Admin → Posts → Add New. The Bricks Single
template handles layout automatically (matches templateConditions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:29:29 +02:00
1ddde1ffd5 feat(header): add About / Customers / Partners submenu under Why Asterion
Extends av-add-submenus.php to attach 3 editorial pages as children of
the 'Why Asterion' top-level menu item :
  Why Asterion
    ├ About
    ├ Customers
    └ Partners

Re-running ?av_add_submenus=1 is idempotent : Solutions / Industries /
Technology children stay as-is (matched by URL), only the new Why Asterion
children get inserted (menu items #150-152).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:19:03 +02:00
a795fb345f style(footer): align nav-menu links with column heading + restore tagline visibility
Adds wp_head CSS overrides to fix two issues :
1. Footer nav-menu links (under Solutions / Company / Legal columns) were
   indented ~40px due to the browser's default <ul> padding-left. Zero it
   out so links align with the gold eyebrow heading above them.
2. Brand tagline "Your Last Mistake Should Be in VR." was using
   var(--color-text-tertiary) which renders too dark on the navy footer.
   Force rgba(255,255,255,0.65) for visible muted-white.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:07:10 +02:00
094fdbea51 style(lists): replace '— ' dash prefix with vertical gold bar accent
The em-dash prefix on list items felt typographic but unbranded. Now each
list item gets a 3px-wide gold vertical bar on the left, evoking a tactical
breadcrumb. Implementation :
- Builders (av_build_list_section in _av-bricks-shared.php +
  av_build_form_two_col promise list in av-generate-forms.php) emit clean
  text without the '— ' prefix, and set _cssClasses = 'av-list-item'
- New mu-plugin av-list-bullet-style.php injects CSS via wp_head() :
  .av-list-item { position: relative; padding-left: 0.9rem; }
  .av-list-item::before { gold bar, 3px × 1.1em, aligned to first line }

Regen all sections (industries, solutions, technology, editorial, forms,
home) to switch existing pages to the new style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:05:45 +02:00
1c297b336f style(footer): align + brand sub-title typography across columns
One-shot mu-plugin patches the Footer template (#46) :
- Column sub-titles (Solutions / Company / Legal / Contact) now use the
  eyebrow style : gold, uppercase, 0.75rem, 0.1em letter-spacing — matches
  the section-eyebrows used across content pages.
- Brand heading (ASTERION VR) downsized to 1.125rem so it stops dominating
  the row; tagline muted to text-tertiary 0.875rem.
- Columns wrapper set to align-items: flex-start with consistent 3rem gap
  → all sub-titles sit at the same vertical baseline.
- Each column container tightened to 0.5rem rowGap (less air between
  heading and links).

Patches both content_2 (JSON, source) and footer_2 (serialized, mirrored
by the REST bridge). Idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:02:10 +02:00
a5c15b4c7a feat(header): custom flag-based language switcher
Replaces WPML's default language selector with a compact branded dropdown :
current language flag + 2-letter code + chevron, with a CSS-only hover/focus
dropdown listing other available languages (flag + native name).

Implementation : remove_all_actions on wpml_add_language_selector + register
our own renderer. Bricks's wpml-language-switcher element (hdrlng) fires the
same action so no template edit needed.

Flags via emoji (🇬🇧 EN, 🇫🇷 FR) — no asset path concerns. Dropdown styled
to match the rest of the header (navy bg, gold-on-hover, subtle border +
shadow). Mobile : dropdown anchors left to stay in viewport.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:59:52 +02:00
b530b7a76f style(header): compact, branded dropdown for nav-menu submenus
CSS injected via wp_head() overrides Bricks default nav-menu sub-menu
spacing (which inherits a bloated line-height + item padding via the
parent menu's settings, not addressable through Bricks's nav-menu
controls alone). Tight padding (0.55rem 1.25rem), branded navy bg with
subtle gold hover, 16rem min-width, soft shadow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:45:35 +02:00
7134b70db9 feat(header): add submenus for Solutions / Industries / Technology + style dropdown
One-shot mu-plugin (?av_add_submenus=1) :
1. Adds child menu items to the Header — Main WP menu (term_id 9) :
   - Solutions → MY PROSERVE, PROSERVE FLEX, PROSERVE ACADEMY, PROSERVE+
   - Industries → Police, Special Forces, Military, Firefighters
   - Technology → VR Hardware, Software & AI, Weapons & Tracking, Scenarios,
     Instructor Cockpit, After-Action Review
   Children are matched by URL → idempotent re-runs.
2. Updates the Bricks nav-menu element settings in template #25 with
   dropdown styling : navy background, gold-on-hover text, padding,
   subtle gold border, indicator arrow. Patches both content_2 (JSON)
   and header_2 (serialized) meta keys.

Bricks's nav-menu CSS automatically handles hover-to-show on .menu-item-has-children.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:20:54 +02:00
c4718658d4 fix(header): make Asterion logo link to home
One-shot mu-plugin patches the Bricks heading 'hdrlgo' in template #25 to
add link.type='external' + link.url='/'. Idempotent — re-runs are safe.
Patches both _bricks_page_content_2 (JSON, source) and _bricks_page_header_2
(serialized PHP, mirrored by the REST bridge), so the change takes effect
immediately without re-running av-fix-template-meta.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:18:37 +02:00
38c0c5031e feat(pages): generate Phases 8-11 — Technology + Editorial + Forms + Home
Brings the site to its full programmatic page count : 24 EN + 24 FR pages
(48 total), all wired into the WPML CTE workflow established in the previous
commit. Each phase reuses tools/_av-bricks-shared.php builders, adds its own
filter('av_fr_translation_dict') to extend the FR dictionary, and ships a
seed-FR mu-plugin for the initial WPML trid linking.

## Phase 8 — Technology (tools/av-generate-technology.php + seed)
Hub /technology/ + 6 pillar pages (vr-hardware, software-ai, weapons-tracking,
scenarios, instructor-cockpit, after-action-review). Hub uses a 6-card grid
(3+3 layout — added card_count=6 case in av_build_cards_section).
EN : 87-93. FR : 94-100.

## Phase 9 — Editorial (tools/av-generate-editorial.php + seed)
5 top-level pages : /why-asterion/, /about/, /customers/, /trust/, /partners/.
Required adding empty-parent-path support to av_upsert_detail_page() so pages
can be created at the site root (no hub). EN : 107-111. FR : 112-116.

## Phase 10 — Forms (tools/av-generate-forms.php + seed)
4 conversion pages : /request-demo/, /request-te-kit/, /request-quote/,
/contact/. Custom page builder (not the shared detail-page pattern) :
hero + 2-column section (Bricks `form` element on left, promise list on
right) + optional shortcuts row (Contact), eligibility list (T&E Kit), FAQ
accordion (Demo). Form actions=email, emailTo=hello@asterionvr.com.
Field labels, select options, FAQ Q&A all in the FR dict.
EN : 122-125. FR : 126-129.

## Phase 11 — Home page (tools/av-generate-home.php + seed)
Custom-written single page (no data file). Sections : hero + 3-pillar cards
(Operational reality / Modular / Sovereign) + 4 Industries cards + 4
Solutions cards + 4-stat numbers band + Why-Asterion teaser CTA + Customers
teaser CTA + custom navy closer. Set as WP static front via
update_option(show_on_front=page, page_on_front=ID).
EN : 134. FR : 135.

## tools/_av-bricks-shared.php
- av_upsert_detail_page() now accepts empty `$parent_path` → top-level page
- av_build_cards_section() handles card_count=6 → 3-per-row layout

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:58:48 +02:00
16 changed files with 2603 additions and 7 deletions

View File

@@ -418,7 +418,8 @@ function av_build_list_section($slug, $i, $section, $bg = 'var(--color-bg-defaul
$els[] = [ $els[] = [
'id' => "i{$slug}l{$i}x{$j}", 'name' => 'text-basic', 'parent' => $list, 'children' => [], 'id' => "i{$slug}l{$i}x{$j}", 'name' => 'text-basic', 'parent' => $list, 'children' => [],
'settings' => [ 'settings' => [
'text' => '— ' . $item, 'text' => $item, // no "— " prefix anymore
'_cssClasses' => 'av-list-item', // gold-bar accent via wp_head CSS
'_typography' => [ '_typography' => [
'color' => av_color('var(--color-text-secondary)'), 'color' => av_color('var(--color-text-secondary)'),
'font-size' => '1rem', 'font-size' => '1rem',
@@ -467,11 +468,11 @@ function av_build_cards_section($slug, $i, $section, $bg = 'var(--color-bg-subtl
$flex_desktop = 'calc((100% - 1.5rem) / 2)'; $flex_desktop = 'calc((100% - 1.5rem) / 2)';
} elseif ($card_count === 3) { } elseif ($card_count === 3) {
$flex_desktop = 'calc((100% - 3rem) / 3)'; $flex_desktop = 'calc((100% - 3rem) / 3)';
} elseif ($card_count === 5) { } elseif ($card_count === 5 || $card_count === 6) {
// 5 cards : we wrap them as 3 + 2 — set flex-basis for 3-up // 5 cards : 3 + 2 — 6 cards : 3 + 3. Both wrap cleanly at 3-per-row.
$flex_desktop = 'calc((100% - 3rem) / 3)'; $flex_desktop = 'calc((100% - 3rem) / 3)';
} else { } else {
// default 4 cards/row (and 6 → 4 + 2 wrapped) // default 4 cards/row (and 7+ → 4 + N wrapped)
$flex_desktop = 'calc((100% - 4.5rem) / 4)'; $flex_desktop = 'calc((100% - 4.5rem) / 4)';
} }
@@ -905,9 +906,14 @@ function av_write_bricks_page_elements($page_id, $elements) {
* Returns ['id' => int, 'action' => 'created'|'updated', 'element_count' => int, 'stored_len' => int]. * Returns ['id' => int, 'action' => 'created'|'updated', 'element_count' => int, 'stored_len' => int].
*/ */
function av_upsert_detail_page($slug, $parent_path, $d) { function av_upsert_detail_page($slug, $parent_path, $d) {
$existing = get_page_by_path("$parent_path/$slug"); // $parent_path empty/null/'' → top-level page (no parent)
$page_path = $parent_path ? "$parent_path/$slug" : $slug;
$existing = get_page_by_path($page_path);
$parent_id = 0;
if ($parent_path) {
$parent = get_page_by_path($parent_path); $parent = get_page_by_path($parent_path);
$parent_id = $parent ? $parent->ID : 0; $parent_id = $parent ? $parent->ID : 0;
}
if ($existing) { if ($existing) {
wp_update_post([ wp_update_post([

151
tools/av-add-submenus.php Normal file
View 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;
});

View 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;
});

View 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;
});

View 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
View 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
View 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'),
];
}

View 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>",
],
];
}

View 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;
}

View 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
View 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);

View 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);

View 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;
});

View 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;
});

View 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;
});

View 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;
});