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>
This commit is contained in:
2026-05-14 19:58:48 +02:00
parent d2badf49d4
commit 38c0c5031e
9 changed files with 1500 additions and 6 deletions

546
tools/av-generate-forms.php Normal file
View File

@@ -0,0 +1,546 @@
<?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' => "\u{2014} " . $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;
}