diff --git a/.docker/Dockerfile b/.docker/Dockerfile
new file mode 100644
index 0000000000..ccb97e4fe5
--- /dev/null
+++ b/.docker/Dockerfile
@@ -0,0 +1,18 @@
+FROM php:8.5-cli
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+# 2. Install system dependencies and Node.js/npm
+# We are using Node.js 22 LTS as a reliable, modern standard
+RUN apt-get update && apt-get install -y \
+ curl gnupg \
+ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
+ && apt-get install -y nodejs \
+ && apt-get clean \
+ && rm -rf /var/lib/lists/*
+
+# 3. Globally install TypeScript and the Socket Dev CLI
+RUN npm install -g typescript socket tsx esbuild
+
+# 4. Set up a default working directory (Optional)
+WORKDIR /app
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..e77124dc23
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,6 @@
+# .dockerignore
+.git
+.gitmodules
+.gitattributes
+.idea
+build-deploy.sh
diff --git a/.router.php b/.router.php
index c204ce635e..98ce5a1773 100644
--- a/.router.php
+++ b/.router.php
@@ -4,12 +4,19 @@
$filename = $_SERVER["PATH_INFO"] ?? $_SERVER["SCRIPT_NAME"];
+//die(print_r($_SERVER, true));
+
+//$_SERVER['HTTP_HOST'] = '';
+//$_SERVER['BASE_PAGE'] = '/';
+//$_SERVER['SERVER_NAME'] = 'localhost';
+
if (!file_exists($_SERVER["DOCUMENT_ROOT"] . $filename)) {
require_once __DIR__ . '/error.php';
return;
}
+
/* This could be an image or whatever, so don't try to compress it */
ini_set("zlib.output_compression", 0);
return false;
diff --git a/bin/createNewsEntry b/bin/createNewsEntry
index 937330218a..f4d8868558 100755
--- a/bin/createNewsEntry
+++ b/bin/createNewsEntry
@@ -180,6 +180,8 @@ function parseOptions(): Entry {
echo " --conf-time 'value' When the event will be occurign (cfp and conference categories only)\n";
echo " --content 'value' Text content for the entry, may include XHTML\n";
echo " --content-file 'value' Name of file to load content from, may not be specified with --content\n";
+ echo " --summary 'value' Short, plain-text summary of the content. Should be a single line.\n";
+ echo " --summary-file 'value' Name of the file to load summary content from, should be a plain-text summary of the content. Should be a single line.\n";
echo " --image-path 'value' Basename of image file in " . Entry::IMAGE_PATH_REL . "\n";
echo " --image-title 'value' Title for the image provided\n";
echo " --image-link 'value' URI to direct to when clicking the image\n";
@@ -238,5 +240,22 @@ function parseOptions(): Entry {
exit(1);
}
+ $summary = $opts['summary'] ?? '';
+ $summaryFile = $opts['summary-file'] ?? '';
+ if ($summary && $summaryFile) {
+ fwrite(STDERR, "--summary and --summary-file may not be specified together\n");
+ exit(1);
+ } elseif ($summaryFile) {
+ $summary = file_get_contents($summaryFile);
+ if ($summary === false) {
+ fwrite(STDERR, "Summary cannot be opened, or must not be empty\n");
+ exit(1);
+ }
+ }
+
+ if ($summary) {
+ $entry->setSummary($summary);
+ }
+
return $entry;
}
diff --git a/bin/sync-pregen.php b/bin/sync-pregen.php
new file mode 100644
index 0000000000..973fd5d28c
--- /dev/null
+++ b/bin/sync-pregen.php
@@ -0,0 +1,37 @@
+
+
+
+
diff --git a/images/landing/php-bugs.png b/images/landing/php-bugs.png
new file mode 100644
index 0000000000..e3f3004315
Binary files /dev/null and b/images/landing/php-bugs.png differ
diff --git a/images/language-flags/br.png b/images/language-flags/br.png
new file mode 100644
index 0000000000..eb88391c1a
Binary files /dev/null and b/images/language-flags/br.png differ
diff --git a/images/language-flags/de.png b/images/language-flags/de.png
new file mode 100644
index 0000000000..0a62147326
Binary files /dev/null and b/images/language-flags/de.png differ
diff --git a/images/language-flags/en.webp b/images/language-flags/en.webp
new file mode 100644
index 0000000000..833789753e
Binary files /dev/null and b/images/language-flags/en.webp differ
diff --git a/images/language-flags/es.png b/images/language-flags/es.png
new file mode 100644
index 0000000000..ce714320f5
Binary files /dev/null and b/images/language-flags/es.png differ
diff --git a/images/language-flags/fr.png b/images/language-flags/fr.png
new file mode 100644
index 0000000000..94a8acbec1
Binary files /dev/null and b/images/language-flags/fr.png differ
diff --git a/images/language-flags/it.png b/images/language-flags/it.png
new file mode 100644
index 0000000000..66568e44fa
Binary files /dev/null and b/images/language-flags/it.png differ
diff --git a/images/language-flags/ja.png b/images/language-flags/ja.png
new file mode 100644
index 0000000000..f009df369b
Binary files /dev/null and b/images/language-flags/ja.png differ
diff --git a/images/language-flags/ru.png b/images/language-flags/ru.png
new file mode 100644
index 0000000000..fdd45a9b75
Binary files /dev/null and b/images/language-flags/ru.png differ
diff --git a/images/language-flags/tr.png b/images/language-flags/tr.png
new file mode 100644
index 0000000000..3510020b54
Binary files /dev/null and b/images/language-flags/tr.png differ
diff --git a/images/language-flags/uk.webp b/images/language-flags/uk.webp
new file mode 100644
index 0000000000..16abefbd4e
Binary files /dev/null and b/images/language-flags/uk.webp differ
diff --git a/images/language-flags/zh.webp b/images/language-flags/zh.webp
new file mode 100644
index 0000000000..fb0a0d1d30
Binary files /dev/null and b/images/language-flags/zh.webp differ
diff --git a/images/logos/composer.png b/images/logos/composer.png
new file mode 100644
index 0000000000..e0782d0593
Binary files /dev/null and b/images/logos/composer.png differ
diff --git a/images/logos/github_invertocat_white.svg b/images/logos/github_invertocat_white.svg
new file mode 100644
index 0000000000..527ba11c50
--- /dev/null
+++ b/images/logos/github_invertocat_white.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/images/logos/php-foundation.svg b/images/logos/php-foundation.svg
new file mode 100644
index 0000000000..cf5b3a6942
--- /dev/null
+++ b/images/logos/php-foundation.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/images/logos/phpc-discord.png b/images/logos/phpc-discord.png
new file mode 100644
index 0000000000..7a79320cc7
Binary files /dev/null and b/images/logos/phpc-discord.png differ
diff --git a/images/logos/reddit.png b/images/logos/reddit.png
new file mode 100644
index 0000000000..9ce7ce2541
Binary files /dev/null and b/images/logos/reddit.png differ
diff --git a/include/branch-meta.inc b/include/branch-meta.inc
new file mode 100644
index 0000000000..c68e1eae25
--- /dev/null
+++ b/include/branch-meta.inc
@@ -0,0 +1,92 @@
+ [
+ 'support_label' => 'Supported',
+ 'features' => [
+ [
+ 'title' => 'URI Extension',
+ 'about' => 'PHP 8.5 adds a built-in URI extension to parse, normalize, and handle URLs following RFC 3986 and WHATWG URL standards.',
+ ],
+ [
+ 'title' => 'Pipe Operator',
+ 'about' => 'The |> operator enables chaining callables left-to-right, passing values smoothly through multiple functions without intermediary variables.',
+ ],
+ [
+ 'title' => 'Clone With',
+ 'about' => 'Clone objects and update properties with the new clone() syntax, making the "with-er" pattern simple for readonly classes.',
+ ],
+ [
+ 'title' => '#[\NoDiscard] Attribute',
+ 'about' => 'The #[\NoDiscard] attribute warns when a return value isn’t used, helping prevent mistakes and improving overall API safety.',
+ ],
+ [
+ 'title' => 'Closures and First-Class Callables in Constant Expressions',
+ 'about' => 'Static closures and first-class callables can now be used in constant expressions, such as attribute parameters.',
+ ],
+ [
+ 'title' => 'Persistent cURL Share Handles',
+ 'about' => 'Handles can now be persisted across multiple PHP requests, avoiding the cost of repeated connection initialization to the same hosts.',
+ ],
+ ],
+ ],
+ '8.4' => [
+ 'support_label' => 'Supported',
+ 'features' => [
+ [
+ 'title' => 'Property Hooks',
+ 'short' => 'Property Hooks allow intercepting properties',
+ ],
+ [
+ 'title' => 'Asymmetric Property Visibility',
+ 'short' => 'Asymmetric Visibility for get and set',
+ ],
+ [
+ 'title' => '#[Derpeciated] Attribute',
+ 'short' => '#[Depreciated] attribute signals removal intent',
+ ],
+ [
+ 'title' => 'Additional Array Functions',
+ 'short' => 'New array lookup and query options',
+ ],
+ ],
+ ],
+ '8.3' => [
+ 'support_label' => 'Security Support',
+ 'features' => [
+ [
+ 'title' => 'Typed Class Constants',
+ 'short' => 'Class constants can now be typed',
+ ],
+ [
+ 'title' => 'Dynamic Class Constants',
+ 'about' => 'Class constants can now be accessed via dynamic calls',
+ ],
+ [
+ 'title' => 'Readonly Deep Cloning',
+ 'short' => 'Enhanced deep cloning of readonly instances',
+ ],
+ [
+ 'title' => 'Randomizer Improvements',
+ 'short' => 'Generate random strings from provided character sets',
+ ],
+ ],
+ ],
+ '8.2' => [
+ 'support_label' => 'Security Support',
+ 'features' => [
+ [
+ 'title' => 'Readonly classes',
+ 'short' => 'Entire classes can now be marked Readonly',
+ ],
+ [
+ 'title' => 'Disjunction Normal Form Types',
+ 'short' => 'Improved type support with Disjunction Normal Forms',
+ ],
+ [
+ 'title' => 'Improved Standalone Types',
+ 'short' => 'Null, true and false are now usable as types',
+ ],
+ ],
+ ],
+];
diff --git a/include/branches.inc b/include/branches.inc
index deb1f79841..bf56899bc4 100644
--- a/include/branches.inc
+++ b/include/branches.inc
@@ -89,6 +89,11 @@ function format_interval($from, DateTime $to) {
return $eolPeriod;
}
+function get_distribution_base_url(): string
+{
+ return 'https://www.php.net/distributions';
+}
+
function version_number_to_branch(string $version): ?string {
$parts = explode('.', $version);
if (count($parts) > 1) {
@@ -138,9 +143,14 @@ function get_all_branches() {
function get_active_branches($include_recent_eols = true) {
$branches = [];
$now = new DateTime();
+ $baseUrl = get_distribution_base_url();
foreach ($GLOBALS['RELEASES'] as $major => $releases) {
foreach ($releases as $version => $release) {
+ foreach ($release['source'] as $sourceId => $source) {
+ $releases[$version]['source'][$sourceId]['url'] = $baseUrl . $source['filename'];
+ }
+
$branch = version_number_to_branch($version);
if ($branch) {
diff --git a/include/communities.inc b/include/communities.inc
new file mode 100644
index 0000000000..5b1dc7d5bd
--- /dev/null
+++ b/include/communities.inc
@@ -0,0 +1,22 @@
+ 'Reddit',
+ 'about' => 'Reddit has an active PHP community discussing the language and its ecosystem.',
+ 'image' => '/images/logos/reddit.png',
+ 'href' => 'https://www.reddit.com/r/PHP/',
+ ],
+ [
+ 'title' => 'PHP Community Discord',
+ 'about' => 'Join thousands of users on Discord talking about PHP.',
+ 'image' => '/images/logos/phpc-discord.png',
+ 'href' => 'https://discord.phpc.social/',
+ ],
+ [
+ 'title' => 'Official Mailing Lists',
+ 'about' => 'Help and guidance, as well as proposals & discussions on the future of the language.',
+ 'image' => '/images/logos/new-php-logo.png',
+ 'href' => '/mailing-lists.php',
+ ],
+];
diff --git a/include/development-links.inc b/include/development-links.inc
new file mode 100644
index 0000000000..21e30dc56c
--- /dev/null
+++ b/include/development-links.inc
@@ -0,0 +1,46 @@
+ 'PHP on Github',
+ 'about' => 'Browse and contribute to the source code behind the PHP engine and infrastructure.',
+ 'image' => '/images/logos/github_invertocat_white.svg',
+ 'href' => 'https://github.com/php',
+ 'href_label' => 'Visit GitHub',
+ ],
+ [
+ 'title' => 'RFCs / Language Proposals',
+ 'about' => 'Requests for Comments are the mechanism PHP internals uses to propose language changes.',
+ 'image' => 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/PHP-logo.svg/500px-PHP-logo.svg.png',
+ 'href' => 'https://wiki.php.net/rfc',
+ 'href_label' => 'View Proposals',
+ ],
+ [
+ 'title' => 'PHP Internals',
+ 'about' => 'Browse discussions from PHP Internals mailing list about current and future enhancements.',
+ 'image' => '/images/logos/new-php-logo.png',
+ 'href' => 'https://news-web.php.net/php.internals',
+ 'href_label' => 'Browse Mailing List',
+ ],
+ [
+ 'title' => 'Get Involved',
+ 'about' => 'Find ways to contribute to the PHP engine, documentation and more.',
+ 'image' => '/images/landing/contribute.png',
+ 'href' => '/get-involved.php',
+ 'href_label' => 'Get Involved',
+ ],
+ [
+ 'title' => 'Submit a Bug Report',
+ 'about' => 'Found a bug in the PHP runtime? Help us out by submitting it to our issue tracker.',
+ 'image' => '/images/landing/php-bugs.png',
+ 'href' => 'https://github.com/php/php-src/issues',
+ 'href_label' => 'Browse & Submit Issues',
+ ],
+ [
+ 'title' => 'Documentation Translation',
+ 'about' => 'Help our team translate our documentation into multiple languages.',
+ 'image' => '/images/landing/docs-translator.png',
+ 'href' => 'https://doc.php.net/guide/',
+ 'href_label' => 'Learn About Translation',
+ ]
+];
diff --git a/include/footer.inc b/include/footer.inc
index faa7305d68..7ddcb50a4f 100644
--- a/include/footer.inc
+++ b/include/footer.inc
@@ -1,4 +1,7 @@
-
+
+
+
+
";
diff --git a/include/header.inc b/include/header.inc
index 48722edafa..92156b4d59 100644
--- a/include/header.inc
+++ b/include/header.inc
@@ -62,7 +62,7 @@ if (!isset($config["languages"])) {
- ">
+
@@ -106,7 +106,7 @@ if (!isset($config["languages"])) {
- ">
+
@@ -357,6 +357,13 @@ if (!isset($config["languages"])) {
+
+
+
+
+
@@ -366,5 +373,7 @@ if (!isset($config["languages"])) {
+
+
diff --git a/include/landing-heros.inc b/include/landing-heros.inc
new file mode 100644
index 0000000000..ed84fb7d3c
--- /dev/null
+++ b/include/landing-heros.inc
@@ -0,0 +1,46 @@
+ 'try-it-now',
+ 'title' => 'Try It Now',
+ 'about' => 'Begin writing PHP code immediately within a browser-based sandbox. No install required.',
+ 'href' => '/sandbox/sandbox.php',
+ 'href_label' => 'Launch Sandbox',
+ ],
+ [
+ 'id' => 'why-use-php',
+ 'title' => 'Why Use PHP?',
+ 'about' => 'Learn why PHP powers the global web - from individual blogs to enormous enterprises.',
+ 'href' => 'https://web-php-pr-1172.preview.thephp.foundation/why-use-php',
+ 'href_label' => 'Discover Why',
+ ],
+ [
+ 'id' => 'learn',
+ 'title' => 'Learn',
+ 'about' => 'Browse the documentation, including extensive tutorials and guidance.',
+ 'href' => '/docs',
+ 'href_label' => 'Browse Docs',
+ ],
+ [
+ 'id' => 'releases',
+ 'title' => 'Releases',
+ 'about' => 'View currently supported PHP runtimes including download logs and highlight features.',
+ 'href' => '#releases',
+ 'href_label' => 'View Runtimes',
+ ],
+ [
+ 'id' => 'community',
+ 'title' => 'Community',
+ 'about' => 'Get involved with the PHP Community via forums, live chat and conferences.',
+ 'href' => '#community',
+ 'href_label' => 'Get Engaged',
+ ],
+ [
+ 'id' => 'language-development',
+ 'title' => 'Language Development',
+ 'about' => 'See how the PHP language works to evolve, and maybe even get involved yourself!',
+ 'href' => '#language-development',
+ 'href_label' => 'Find Out More',
+ ],
+];
diff --git a/include/layout.inc b/include/layout.inc
index b9cc7c0b63..0455508881 100644
--- a/include/layout.inc
+++ b/include/layout.inc
@@ -495,7 +495,6 @@ META
if (isset($_SERVER['BASE_PAGE']) && $shortname = get_shortname($_SERVER["BASE_PAGE"])) {
$shorturl = "https://www.php.net/" . $shortname;
}
-
require __DIR__ . "/header.inc";
}
function site_footer(array $config = []): void
diff --git a/include/newer-version-available.php b/include/newer-version-available.php
new file mode 100644
index 0000000000..9eaf7f3176
--- /dev/null
+++ b/include/newer-version-available.php
@@ -0,0 +1,5 @@
+
+ There's an even better version of PHP available!
+
+ Click here to find out more.
+
diff --git a/include/php-foundation-sponsors.inc b/include/php-foundation-sponsors.inc
new file mode 100644
index 0000000000..03cd5acb3b
--- /dev/null
+++ b/include/php-foundation-sponsors.inc
@@ -0,0 +1,24 @@
+ 'JetBrains', 'icon' => 'https://images.opencollective.com/jetbrains/fe76f99/logo.png'],
+ ['name' => 'Private Packagist', 'icon' => 'https://images.opencollective.com/packagist/2ac48ff/logo.png'],
+ ['name' => 'Cybozu', 'icon' => 'https://images.opencollective.com/cybozu/933e46d/logo.png'],
+ ['name' => 'Aternos GmbH', 'icon' => 'https://images.opencollective.com/aternos/5436b31/logo.png'],
+ ['name' => 'Mercari Inc.', 'icon' => 'https://images.opencollective.com/mercari/454ef50/logo.png'],
+ ['name' => 'pixiv Inc.', 'icon' => 'https://images.opencollective.com/user-ecfec7e5/2f4c2c4/logo.png'],
+ ['name' => 'SPY', 'icon' => 'https://images.opencollective.com/spy/261d722/logo.png'],
+ ['name' => 'Symfony Corp', 'icon' => 'https://images.opencollective.com/symfony-sas/b1f53fd/logo.png'],
+ ['name' => 'shopware AG', 'icon' => 'https://images.opencollective.com/stefan-hamann/2865d41/logo.png'],
+ ['name' => 'OP.GG', 'icon' => 'https://images.opencollective.com/opgg/7e44af2/logo.png'],
+ ['name' => 'Passbolt', 'icon' => 'https://images.opencollective.com/passbolt/2468aab/logo.png'],
+ ['name' => 'Spryker', 'icon' => 'https://images.opencollective.com/spryker/a634346/logo.png'],
+ ['name' => 'Digital Scholar', 'icon' => 'https://images.opencollective.com/digital-scholar/logo.png'],
+ ['name' => 'Cambium Learning, Inc.', 'icon' => 'https://images.opencollective.com/cambium-learning-inc/30c5f1c/logo.png'],
+ ['name' => 'Craft CMS', 'icon' => 'https://images.opencollective.com/craftcms/1fd28bf/logo.png'],
+ ['name' => 'GoDaddy.com', 'icon' => 'https://images.opencollective.com/godaddy/c37e587/logo.png'],
+ ['name' => 'Laravel', 'icon' => 'https://images.opencollective.com/laravel/4ad04b8/logo.png'],
+ ['name' => 'Livesport s.r.o.', 'icon' => 'https://images.opencollective.com/livesport-s-r-o/be081c5/logo.png'],
+ ['name' => 'Aligent Consulting', 'icon' => 'https://images.opencollective.com/aligent-consulting/ee7abd9/logo.png'],
+ ['name' => 'Moodle', 'icon' => 'https://images.opencollective.com/moodle/141a57d/logo.png'],
+];
diff --git a/include/prepend.inc b/include/prepend.inc
index b2f3969f11..6409202756 100644
--- a/include/prepend.inc
+++ b/include/prepend.inc
@@ -121,3 +121,8 @@ function google_cse(): void {
echo $cse_snippet;
}
+
+function safe(string $html): string
+{
+ return htmlspecialchars($html, encoding: 'UTF-8');
+}
diff --git a/include/site.inc b/include/site.inc
index aae24537a6..61fa38005c 100644
--- a/include/site.inc
+++ b/include/site.inc
@@ -244,7 +244,14 @@ if (!isset($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] != "on") {
$proto = "https";
}
-if ($_SERVER["SERVER_PORT"] != '80' && $_SERVER["SERVER_PORT"] != 443) {
+$hostHeader = $hostHeader = $_SERVER["HTTP_HOST"] ?? null;
+if ($hostHeader
+ /* incoming hostname validation logic */
+ && preg_match('/^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)*[a-z0-9]([a-z0-9-]*[a-z0-9])?(?::\d{1,5})?$/i', $hostHeader)
+) {
+ $MYSITE = $proto . '://' . $hostHeader . '/';
+ $msite = 'https://' . $hostHeader . '/';
+} elseif ($_SERVER["SERVER_PORT"] != '80' && $_SERVER["SERVER_PORT"] != 443) {
$MYSITE = $proto . '://' . $_SERVER["SERVER_NAME"] . ':' . (int)$_SERVER["SERVER_PORT"] . '/';
$msite = 'http://' . $_SERVER["SERVER_NAME"] . ':' . (int)$_SERVER["SERVER_PORT"] . '/';
} else {
diff --git a/index.php b/index.php
index 9e24fc8c5f..c8667b3204 100644
--- a/index.php
+++ b/index.php
@@ -1,5 +1,6 @@
";
-foreach ((new NewsHandler())->getFrontPageNews() as $entry) {
- $link = preg_replace('~^(http://php.net/|https://www.php.net/)~', '', $entry["id"]);
- $id = parse_url($entry["id"], PHP_URL_FRAGMENT);
- $date = date_create($entry['updated']);
- $date_human = date_format($date, 'd M Y');
- $date_w3c = date_format($date, DATE_W3C);
- $content .= <<
-
-
- {$entry["content"]}
-
-
-NEWSENTRY;
-}
-$content .= 'Older News Entries
';
-$content .= " ";
-
-$intro = <<
-
- A popular general-purpose scripting language that is especially suited to web development. Fast, flexible and pragmatic, PHP powers everything from your blog to the most popular websites in the world.
-
-EOF;
-
-$intro .= "\n";
$active_branches = get_active_branches();
+$active_branches_sorted = [];
+
+/** @var array}> $branch_descriptors */
+$branch_descriptors = require __DIR__ . '/include/branch-meta.inc';
+
krsort($active_branches);
foreach ($active_branches as $major => $releases) {
- krsort($releases);
- foreach ((array)$releases as $release) {
+ ksort($releases);
+ $releases = array_reverse($releases);
+
+ foreach ($releases as $release) {
$version = $release['version'];
[$major, $minor, $_] = explode('.', $version);
- $intro .= "
- $version · Changelog · Upgrading \n";
+ $versionLabel = $major . '.' . $minor;
+ $branch = $branch_descriptors[$versionLabel] ?? [];
+
+ $active_branches_sorted[] = [
+ ...$release,
+ 'major' => $major,
+ 'minor' => $minor,
+ 'version_ex' => $major . '.'. $minor,
+ 'label' => $versionLabel,
+ 'download_url' => '/downloads.php?version=' . $versionLabel,
+ 'more_url' => '/releases/' . $versionLabel . '/en.php',
+ 'changelog_url' => '/ChangeLog-' . $major . '.php#' . $version,
+ 'migration_url' => '/migration' . $major . $minor,
+ 'security_eol' => get_branch_security_eol_date($versionLabel),
+ 'support_eol' => get_branch_bug_eol_date($versionLabel),
+ 'meta' => $branch,
+ 'logo' => '/images/php8/logo_php' . $major . '_' . $minor . '.svg',
+ 'features' => [
+ ...($branch['features'] ?? []),
+ ]
+ ];
+ }
+}
+
+$latest = array_shift($active_branches_sorted);
+
+function buildNavCard(NavCardItem $card, array $config = []): string
+{
+ $config = [
+ 'cn_card' => 'landing-cc-card',
+ 'cn_card_content' => 'landing-cc-card-content',
+ 'cn_card_img' => 'landing-cc-card-img',
+ ...$config,
+ ];
+
+ ob_start();
+ ?>
+ id) { ?>id="= safe($card->id) ?>" } ?> href="= safe($card->href) ?>" class="vgrid-card landing-card-ovh = $config['cn_card'] ?>">
+
+
+ image) { ?>
+
= safe($card->title) ?>
+
+
+
= safe($card->about) ?>
+
+ href) { ?>
+
+ = safe($card->href_label)?>
+
+
+
+
+ buildNavCard($card, $config), $cards));
+}
+
+function drawBranchInfo(array $release): void
+{
+ $now = new DateTime();
+ $fmtDate = fn(DateTime $date) => ($date < $now) ? 'End of Life' : $date->format('Y-m-d');
+
+ ?>
+
+
+
+
+
+ Bugfixes:
+ = safe($fmtDate($release['support_eol'])) ?>
+
+
+ Security:
+ = safe($fmtDate($release['security_eol'])) ?>
+
+
+
+
+ getConferences() as $conf) {
+ $finalTeaserDate = $conf['finalTeaserDate'] ?? null;
+ if (!$finalTeaserDate) {
+ continue;
+ }
+
+ if (DateTimeImmutable::createFromFormat('Y-m-d', $finalTeaserDate) < $now) {
+ continue;
+ }
+
+ $link = array_find($conf['link'], fn(array $conf) => ($conf['rel'] ?? 'alternate') === 'alternate');
+ if ($link) {
+ $link = str_replace('https://www.php.net', '', $link['href']);
+ }
+
+ $id = parse_url($conf["id"], PHP_URL_FRAGMENT);
+ $image = $conf["newsImage"]['content'] ?? null;
+ if ($image) {
+ $image = '/images/news/' . $image;
}
+
+ $eventCards[] = new NavCardItem(
+ title: $conf['title'],
+ about: $conf['summary'] ?? '',
+ image: $image,
+ href: $link ?? '/conferences/',
+ href_label: 'Explore Event',
+ );
+
+ if (count($eventCards) > $MAX_CONFERENCE_CARDS) {
+ break;
+ }
+}
+
+$developmentCards = [];
+foreach (require __DIR__ . '/include/development-links.inc' as $community) {
+ $developmentCards[] = new NavCardItem(
+ title: $community['title'],
+ about: $community['about'],
+ image: $community['image'],
+ href: $community['href'],
+ href_label: $community['href_label'] ?? 'Visit Community',
+ );
}
-$intro .= " \n";
-$intro .= <<
-EOF;
+
+$heroCards = [];
+foreach (require __DIR__ . '/include/landing-heros.inc' as $hero) {
+ $heroCards[] = new NavCardItem(
+ title: $hero['title'],
+ about: $hero['about'],
+ image: $entry["newsImage"]["content"] ?? '',
+ href: $hero['href'],
+ href_label: $hero['href_label'],
+ id: isset($hero['id']) ? ('hero-' . $hero['id']) : null,
+ );
+}
+
+$foundationSponsors = require __DIR__ . '/include/php-foundation-sponsors.inc';
+
+ob_start();
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Fast & Modern
+
PHP provides blistering fast performance and a modern developer-focused experience.
+
+
+
+
A Massive Ecosystem
+
Leverage over 450,000 existing open source packages for your projects, along with powerful tooling.
+
+
+
+
An Established Community
+
Millions of developers and businesses already use PHP to achieve their goals every day.
+
+
+
+
+
+
+
= buildNavCards($heroCards, ['cn_card' => 'landing-cc-card landing-cc-card-alt']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Major Features
+
+
+
+
= safe($change['title'])?>
+
= safe($change['about'] ?? $change['short']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Major Features:
+
+
+ = safe($change['short'] ?? $change['title']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The PHP Foundation is grateful for our many sponsors, including:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Community
+
+
+
+
+
+
Events & Conferences
+
+
= buildNavCards($eventCards, ['cn_card' => 'landing-cc-card landing-cc-card-grey']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PHP has one of the largest collections of open-source libraries in the world.
+
+
+ Ranging from individual helpers to entire application frameworks, all packages are easily installable via the Composer
+ package manager.
+
+
+
+
Get Composer
+ ·
+
Browse Package Repository
+
+
+
+
+
+
+
+
Language Development
+
+ = buildNavCards($developmentCards, ['cn_card' => 'landing-cc-card landing-cc-card-grey']) ?>
+
+
+
+
+
+
+
+ ['home.css'],
- 'intro' => $intro,
+ 'include_section' => false,
],
);
-// Print body of home page.
-echo $content;
-
-// Prepare announcements.
-if (is_array($CONF_TEASER)) {
- $conftype = [
- 'conference' => 'Upcoming conferences',
- 'cfp' => 'Conferences calling for papers',
- ];
- $announcements = "";
- foreach ($CONF_TEASER as $category => $entries) {
- if ($entries) {
- $announcements .= '';
- $announcements .= '
' . $conftype[$category] . ' ';
- $announcements .= '
';
- foreach (array_slice($entries, 0, 4) as $url => $title) {
- $title = preg_replace("'([A-Za-z0-9])([\s:\-,]*?)call for(.*?)$'i", "$1", $title);
- $announcements .= "$title ";
- }
- $announcements .= ' ';
- $announcements .= '
';
- }
- }
-} else {
- $announcements = '';
-}
-
-$SIDEBAR = <<
- The PHP Foundation
-
-
The PHP Foundation is a collective of people and organizations, united in the mission to ensure the long-term prosperity of the PHP language.
-
Donate
-
-
-$announcements
-
User Group Events
-
Special Thanks
-
-
-SIDEBAR_DATA;
+echo $header;
// Print the common footer.
site_footer([
+ 'include_section' => false,
"atom" => "/feed.atom", // Add a link to the feed at the bottom
- 'elephpants' => true,
- 'sidebar' => $SIDEBAR,
]);
diff --git a/js/common.js b/js/common.js
index 86b4862fd5..c00fc103eb 100644
--- a/js/common.js
+++ b/js/common.js
@@ -879,3 +879,23 @@ function applyTheme(theme) {
}
applyTheme(savedTheme)
+
+function shuffleImmutableArray(array) {
+ const newArray = [...array];
+ for (let i = newArray.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
+ }
+ return newArray;
+}
+
+function shuffleDOMChildrenWithLimit(parent, limit) {
+ const children = Array.from(parent.children);
+ const replacements = shuffleImmutableArray(children.slice(0, limit));
+
+ while (parent.children.length) {
+ parent.removeChild(parent.children[0]);
+ }
+
+ replacements.forEach(n => parent.appendChild(n));
+}
diff --git a/js/landing.js b/js/landing.js
new file mode 100644
index 0000000000..c57dd02572
--- /dev/null
+++ b/js/landing.js
@@ -0,0 +1,80 @@
+function initInfiniteScroll(parentContainer, speed = 50) {
+ if (!parentContainer) return null;
+
+ // 1. Extract the fixed width from the data-width attribute
+ const dataWidth = parentContainer.dataset.width;
+ if (!dataWidth) {
+ console.error("InfiniteScroll Error: Missing 'data-width' attribute on the container.");
+ return null;
+ }
+
+ const widthNum = parseInt(dataWidth, 10);
+ const widthStr = `${widthNum}px`;
+
+ // 2. Gather the existing child elements currently inside the parent
+ const elements = Array.from(parentContainer.children);
+ if (elements.length === 0) return null;
+
+ // 3. Prepare the parent container styling
+ parentContainer.style.overflow = 'hidden';
+ parentContainer.style.position = 'relative';
+ parentContainer.style.width = '100%';
+
+ // 4. Create the scrolling track
+ const track = document.createElement('div');
+ track.style.display = 'flex';
+ track.style.width = 'max-content';
+ track.style.willChange = 'transform';
+
+ // 5. Apply widths and move existing elements into the track
+ elements.forEach(el => {
+ el.style.width = widthStr;
+ el.style.flexShrink = '0';
+ track.appendChild(el); // Automatically removes it from parentContainer and places it in track
+ });
+
+ // 6. Append the track to the parent container
+ parentContainer.appendChild(track);
+
+ // 7. Clone elements to ensure a seamless loop with no blank gaps
+ const parentWidth = parentContainer.offsetWidth || window.innerWidth;
+ const originalTotalWidth = elements.length * widthNum;
+
+ let currentTrackWidth = originalTotalWidth;
+ while (currentTrackWidth < parentWidth + originalTotalWidth) {
+ elements.forEach(el => {
+ const clone = el.cloneNode(true);
+ clone.style.width = widthStr;
+ clone.style.flexShrink = '0';
+ track.appendChild(clone);
+ });
+ currentTrackWidth += originalTotalWidth;
+ }
+
+ // 8. Generate a unique CSS keyframe animation dynamically
+ const animationName = `infiniteScroll_${Math.random().toString(36).substr(2, 9)}`;
+ const styleNode = document.createElement('style');
+ styleNode.textContent = `
+ @keyframes ${animationName} {
+ 0% { transform: translateX(0); }
+ 100% { transform: translateX(-${originalTotalWidth}px); }
+ }
+ `;
+ document.head.appendChild(styleNode);
+
+ // 9. Apply the animation (speed is pixels per second)
+ const duration = originalTotalWidth / speed;
+ track.style.animation = `${animationName} ${duration}s linear infinite`;
+
+ // 10. Return playback and cleanup controls
+ return {
+ pause: () => track.style.animationPlayState = 'paused',
+ play: () => track.style.animationPlayState = 'running',
+ destroy: () => {
+ // Puts original elements back and cleans up the DOM
+ elements.forEach(el => parentContainer.appendChild(el));
+ track.remove();
+ styleNode.remove();
+ }
+ };
+}
diff --git a/js/sandbox.js b/js/sandbox.js
new file mode 100644
index 0000000000..5c9768426a
--- /dev/null
+++ b/js/sandbox.js
@@ -0,0 +1,41 @@
+import phpBinary from "./php-web.mjs";
+
+export class PHPSandbox {
+ constructor(templateFiles) {
+ this.templateFiles = templateFiles;
+ }
+
+ async execute(files) {
+ let buffer = [];
+ let initializing = true;
+
+ files = {...files, ...this.templateFiles};
+
+ const php = await phpBinary({
+ print(data) {
+ if (initializing) {
+ return;
+ }
+
+ console.log('output', data);
+
+ buffer.push(data);
+ }
+ });
+
+ for (const [filename, content] of Object.entries(files)) {
+ const dir = filename.substring(0, filename.lastIndexOf('/'));
+ if (dir) {
+ php.FS_createPath('/', dir, true, true);
+ }
+
+ php.FS.writeFile('/' + filename, content);
+ }
+
+ initializing = false;
+ php.ccall("phpw_run", null, ["string"], ['require "boot.php";']);
+
+ return JSON.parse(buffer.join(""));
+ }
+}
+
diff --git a/releases/8.0/release.inc b/releases/8.0/release.inc
index 84e5115ac7..c3dcfef158 100644
--- a/releases/8.0/release.inc
+++ b/releases/8.0/release.inc
@@ -31,12 +31,11 @@ $expectedText = message('this_is_expected', $lang);
-
= message('main_title', $lang) ?>
= message('main_subtitle', $lang) ?>
-
+
+
+
diff --git a/releases/8.1/release.inc b/releases/8.1/release.inc
index 2485d64995..ed12bf8d22 100644
--- a/releases/8.1/release.inc
+++ b/releases/8.1/release.inc
@@ -28,12 +28,11 @@ common_header(message('common_header', $lang));
- = message('main_title', $lang) ?>
= message('main_subtitle', $lang) ?>
-
+
+
+
diff --git a/releases/8.2/release.inc b/releases/8.2/release.inc
index 7fbacf9a4b..2fa856ec15 100644
--- a/releases/8.2/release.inc
+++ b/releases/8.2/release.inc
@@ -25,12 +25,11 @@ common_header(message('common_header', $lang));
- = message('main_title', $lang) ?>
= message('main_subtitle', $lang) ?>
-
+
+
+
diff --git a/releases/8.3/release.inc b/releases/8.3/release.inc
index 0277ce66d6..a01f06b0d7 100644
--- a/releases/8.3/release.inc
+++ b/releases/8.3/release.inc
@@ -25,12 +25,11 @@ common_header(message('common_header', $lang));
- = message('main_title', $lang) ?>
= message('main_subtitle', $lang) ?>
-
+
+
+
diff --git a/releases/8.4/release.inc b/releases/8.4/release.inc
index d83664812c..25353b3a94 100644
--- a/releases/8.4/release.inc
+++ b/releases/8.4/release.inc
@@ -26,12 +26,10 @@ common_header(message('common_header', $lang));
- = message('main_title', $lang) ?>
= message('main_subtitle', $lang) ?>
-
+
+
diff --git a/sandbox/boot.inc b/sandbox/boot.inc
new file mode 100644
index 0000000000..5a17b78e52
--- /dev/null
+++ b/sandbox/boot.inc
@@ -0,0 +1,112 @@
+ */
+ public array $error_logs = [];
+
+ public static function shared(): self
+ {
+ return self::$gs ??= new self();
+ }
+
+ public function __construct()
+ {
+ set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
+ $this->error_logs[] = [
+ 'errno' => $errno,
+ 'message' => $errstr,
+ 'file' => $errfile,
+ 'line' => $errline,
+ ];
+ });
+ }
+
+ public function formatException(Throwable $e): array
+ {
+ $stack = [[
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]];
+
+ foreach ($e->getTrace() as $trace) {
+ $where = [];
+
+ if (isset($trace['class'])) {
+ $where[] = $trace['class'];
+ }
+
+ if (isset($trace['function'])) {
+ $where[] = $trace['function'];
+ }
+
+
+ $stack[] = [
+ 'file' => $trace['file'],
+ 'line' => (int)$trace['line'],
+ ];
+
+ $stack[count($stack) - 2]['where'] = implode('->', $where);
+ }
+
+ /** @var array> $fileLineCache */
+ $fileLineCache = [];
+ foreach ($stack as $idx => $item) {
+ if (!$item['file'] || !file_exists($item['file'])) {
+ $stack[$idx]['snippet'] = null;
+ continue;
+ }
+
+ $lines = $fileLineCache[$item['file']] ??= explode("\n", file_get_contents($item['file']));
+ $stack[$idx]['snippet'] = trim($lines[(int)$item['line'] - 1] ?? '');
+ }
+
+ return [
+ 'message' => $e->getMessage(),
+ 'stack' => $stack,
+ 'previous' => $e->getPrevious() ? $this->formatException($e->getPrevious()) : null,
+ ];
+ }
+
+ public function run()
+ {
+ $result = [
+ 'response_type' => 'text/plain',
+ ];
+
+ $this->error_logs = [];
+ $output = null;
+ $result = null;
+ $path = 'success';
+
+ try {
+ ob_start();
+ require "./entry.php";
+ $result['mode'] = 'success';
+ $result['buffer'] = ob_get_clean();
+ } catch (Throwable $e) {
+ $result['mode'] = 'success';
+ $result['exception'] = $this->formatException($e);
+ $result['buffer'] = (string)$e;
+ ob_get_clean();
+ }
+
+ $result['errors'] = $this->error_logs;
+
+ echo json_encode($result);
+ }
+}
+
+GlobalSandbox::shared()->run();
+
+
+
diff --git a/sandbox/example.txt b/sandbox/example.txt
new file mode 100644
index 0000000000..07d684c257
--- /dev/null
+++ b/sandbox/example.txt
@@ -0,0 +1,37 @@
+
+
+
+
PHP Sandbox
+
+
+
+
+
+
+
+
+
+
An Exception Occurred
+
+
+
+
+
Stack Trace
+
The stack trace shows the path the code took before it encountered the error. The last code to execute is at the top.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'Chinese (Simplified)',
];
+ public const ACTIVE_ONLINE_LANGUAGES_EX = [
+ 'en' => [
+ 'label_en' => 'English',
+ 'label_loc' => 'English',
+ 'icon' => '/images/language-flags/en.webp',
+ ],
+ 'de' => [
+ 'label_en' => 'German',
+ 'label_loc' => 'Deutsch',
+ 'icon' => '/images/language-flags/de.png',
+ ],
+ 'es' => [
+ 'label_en' => 'Spanish',
+ 'label_loc' => 'Español',
+ 'icon' => '/images/language-flags/es.png',
+ ],
+ 'fr' => [
+ 'label_en' => 'French',
+ 'label_loc' => 'Français',
+ 'icon' => '/images/language-flags/fr.png',
+ ],
+ 'it' => [
+ 'label_en' => 'Italian',
+ 'label_loc' => 'Italiano',
+ 'icon' => '/images/language-flags/it.png',
+ ],
+ 'ja' => [
+ 'label_en' => 'Japanese',
+ 'label_loc' => '日本語',
+ 'icon' => '/images/language-flags/ja.png',
+ ],
+ 'pt_BR' => [
+ 'label_en' => 'Brazilian Portuguese',
+ 'label_loc' => 'Português (Brasil)',
+ 'icon' => '/images/language-flags/br.png',
+ ],
+ 'ru' => [
+ 'label_en' => 'Russian',
+ 'label_loc' => 'Русский',
+ 'icon' => '/images/language-flags/ru.png',
+ ],
+ 'tr' => [
+ 'label_en' => 'Turkish',
+ 'label_loc' => 'Türkçe',
+ 'icon' => '/images/language-flags/tr.png',
+ ],
+ 'uk' => [
+ 'label_en' => 'Ukrainian',
+ 'label_loc' => 'Українська',
+ 'icon' => '/images/language-flags/uk.webp',
+ ],
+ 'zh' => [
+ 'label_en' => 'Chinese (Simplified)',
+ 'label_loc' => '简体中文',
+ 'icon' => '/images/language-flags/zh.webp',
+ ],
+ ];
+
/**
* Convert between language codes back and forth
*
diff --git a/src/Navigation/NavCardItem.php b/src/Navigation/NavCardItem.php
new file mode 100644
index 0000000000..22beef36dd
--- /dev/null
+++ b/src/Navigation/NavCardItem.php
@@ -0,0 +1,16 @@
+title = $title;
return $this;
@@ -96,6 +98,14 @@ public function setContent(string $content): self {
return $this;
}
+ public function setSummary(string $content): self {
+ if (empty($content)) {
+ throw new \Exception('Summary must not be empty');
+ }
+ $this->summary = $content;
+ return $this;
+ }
+
public function getId(): string {
return $this->id;
}
@@ -138,6 +148,10 @@ public function save(): self {
$content = self::ce($dom, "content", null, [], $item);
+ if ($this->summary !== '') {
+ self::ce($dom, "summary", $this->summary, [], $item);
+ }
+
// Slurp content into our DOM.
$tdoc = new \DOMDocument("1.0", "utf-8");
$tdoc->formatOutput = true;
diff --git a/styles/landing.css b/styles/landing.css
new file mode 100644
index 0000000000..dfdb3f338d
--- /dev/null
+++ b/styles/landing.css
@@ -0,0 +1,504 @@
+.landing {
+ --card-radius: 0.5em;
+ background-image: url(/images/bg-texture-00.svg);
+}
+
+.landing-dark {
+ --promotion-bg: #252525;
+ --promotion-color: whitesmoke;
+ --promotion-border: #333333;
+ --section-title: whitesmoke;
+ --leader-color: whitesmoke;
+
+ --card1-bg: #444444;
+ --card1-color: whitesmoke;
+
+ --card2-bg: #303030;
+ --card2-color: whitesmoke;
+}
+
+.landing-light {
+ background-color: #e5e9f3;
+ --promotion-bg: #cccccc;
+ --promotion-color: #333333;
+ --promotion-border: #cacaca;
+ --section-title: #222222;
+ --leader-color: #222222;
+
+ --card1-bg: #eeeeee;
+ --card1-color: #222222;
+
+ --card2-bg: #b7b7b7;
+ --card2-color: #222222;
+}
+
+.landing-hdr-outer {
+ color: var(--leader-color);
+}
+
+.landing-hdr {
+ margin-bottom: 3em;
+}
+
+.landing-hero-cards-container {
+ background-color: var(--promotion-bg);
+}
+
+/*
+ * HEADER
+ * Contains the giant PHP and our 3x lead elements
+ */
+
+@media (min-width: 901px) {
+ .landing-hdr {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 1.5em;
+ }
+}
+
+@media (max-width: 900px) {
+ .landing-hdr {
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+ }
+}
+
+.landing-hdr-block {
+ padding: 1em;
+}
+
+.landing-hdr-block + .landing-hdr-block {
+ border-top: 1px dashed #4a5568;
+}
+
+.landing-hdr-title {
+ font-size: larger;
+ margin-bottom: 0.25em;
+}
+
+.landing-hdr-tagline {
+ margin-bottom: 0;
+ font-size: 24px;
+}
+
+.landing-hdr-content {
+
+}
+
+/*
+ * VCARDS
+ * The primary card layout used on the page; automatically collapses into a denser list on mobile.
+ */
+
+.vgrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
+ gap: 1.5em;
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.vgrid-card {
+ border-radius: var(--card-radius);
+ overflow: hidden;
+}
+
+@media (max-width: 700px) {
+ .vgrid {
+ gap: 0;
+ overflow: hidden;
+ border-radius: var(--card-radius);
+ animation: ease;
+ }
+
+ .vgrid-card {
+ border-radius: 0 !important;
+ overflow: hidden;
+ }
+
+ .vgrid-card + .vgrid-card {
+ margin-top: 3px;
+ }
+}
+
+.landing-ver-hero-card {
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+.landing-ver-hero-card-inner {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ overflow: hidden;
+ background: var(--card1-bg);
+ color: var(--card1-color);
+ gap: 1em;
+ padding: 1em;
+}
+
+.landing-ver-hero-img {
+ width: 100%;
+ height: 60px;
+ object-fit: contain;
+ object-position: center center;
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+.landing-light .landing-ver-hero-img {
+ background: #aaaaaa;
+ padding: 1em;
+ border-radius: var(--card-radius);
+ box-sizing: border-box;
+}
+
+@media (max-width: 400px) {
+ .landing-ver-hero-img {
+ height: 40px;
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
+ }
+}
+
+.landing-ver-hero-featuring {
+ font-weight: bold;
+ margin-bottom: 0.25em;
+}
+
+.landing-ver-hero-latest {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em;
+ text-align: center;
+}
+
+.landing-ver-hero-features {
+ margin-bottom: 0;
+}
+
+.landing-ver-hero-label {
+ display: inline-flex;
+ padding: 0.25em 0.75em;
+ border-radius: 0.5em;
+ font-size: 90%;
+}
+
+.landing-ver-hero-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25em;
+ width: 100%;
+}
+
+/*
+ * CARD LAYOUT
+ */
+
+.landing-cc-card {
+ all: unset;
+ /*background: #3c4053;*/
+ border-radius: var(--card-radius);
+ display: flex;
+ flex-direction: row;
+ flex-grow: 1;
+ gap: 0.25em;
+ overflow: hidden;
+ cursor: pointer;
+ color: white;
+ outline: 3px transparent;
+ transition: all 0.2s ease-in-out;
+}
+
+.landing-cc-card:hover {
+ outline: 3px solid #53576d;
+ /*background: #53576d;*/
+ overflow: hidden;;
+ transition: all 0.2s ease-in-out;
+}
+
+.landing-cc-card:focus {
+ outline: 3px solid #eeeeee;
+}
+
+.landing-cc-card-alt {
+ background: var(--card2-bg) !important;
+ color: var(--card2-color) !important;
+}
+
+.landing-cc-card-grey {
+ background: var(--card1-bg) !important;
+ color: var(--card1-color) !important;
+}
+
+.landing-cc-card-grey:hover {
+ background: var(--card1-bg) !important;
+ outline: 3px solid #555555;
+}
+
+.landing-cc-card-img {
+ height: 80px;
+ width: 80px;
+ object-fit: contain;
+ overflow: hidden;
+}
+
+.landing-cc-card-content {
+ /*color: #eeeeee;*/
+ padding: 1em;
+ flex: 1 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+}
+
+.landing-cc-card-title {
+ font-size: 125%;
+ font-weight: 500;
+}
+
+.landing-cc-card-body {
+ flex: 1 1;
+}
+
+/*
+ * LAST RELEASE HERO CARD
+ */
+
+.landing-lrv {
+ overflow: hidden;
+ position: relative;
+ background: #4F5B93;
+ border-radius: var(--card-radius);
+ margin: 0 auto 2em;
+ width: min(1440px, 100%);
+ border: 1px solid #555555;
+}
+
+.landing-lrv-animate {
+ position: absolute;
+ inset: 0;
+ opacity: 0.5;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .landing-lrv-animate {
+ display: none;
+ }
+}
+
+.landing-lrv-inner-padding {
+ position: relative;
+}
+
+@media (max-width: 900px) {
+ .landing-lrv-inner-padding {
+ padding: 1em;
+ }
+
+ .landing-lrv-inner {
+ display: flex;
+ flex-direction: column;
+ gap: 2em;
+ }
+}
+
+@media (min-width: 901px) {
+ .landing-lrv-inner-padding {
+ padding: 2em;
+ }
+
+ .landing-lrv-inner {
+ display: grid;
+ gap: 2em;
+ align-items: center;
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+.landing-lrv-highlights {
+ display: grid;
+ gap: 1em;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+}
+
+.landing-lrv-highlight {
+ background: #44444477;
+ color: white;
+ border-radius: 0.5em;
+ padding: 1em;
+ font-size: smaller;
+}
+
+.landing-lrv-highlight-title {
+ font-weight: 500;
+}
+
+/*
+ * ECOSYSTEM BANNER
+ * Full-width banner intended to promote ecosystem components as a single element
+ */
+
+.landing-eco-full-container {
+ background: var(--promotion-bg);
+ border-top: 3px solid var(--promotion-border);
+ border-bottom: 3px solid var(--promotion-border);
+ color: var(--promotion-color);
+}
+
+.landing-eco-banner {
+ display: flex;
+ flex-direction: row;
+ gap: 2em;
+ align-items: center;
+ padding: 0 5em;
+ color: var(--promotion-color);
+}
+
+@media (max-width: 700px) {
+ .landing-eco-banner {
+ flex-direction: column;
+ padding: 0;
+ gap: 1em;
+ text-align: center;
+ }
+}
+
+.landing-eco-text {
+ font-size: 24px;
+ line-height: 1.3;
+}
+
+/*
+ * SECTIONS
+ */
+
+.landing-section {
+ width: min(1440px, 100%);
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 2em;
+ padding: 5em 1em;
+ box-sizing: border-box;
+}
+
+@media (max-width: 600px) {
+ .landing-section {
+ padding: 1em 1em;
+ }
+}
+
+.landing-section-header {
+ font-size: 18px;
+ text-align: center;
+ text-decoration: none !important;
+ margin: 0;
+ padding: 0;
+ color: var(--section-title);
+ line-height: 1.3;
+ font-weight: 500;
+}
+
+
+/*
+ * MICRO LABEL
+ * Key-Value label intended to be used for versions
+ */
+
+.landing-ml {
+ font-size: smaller;
+ border-radius: 0.75em;
+ border: 1px solid #77777755;
+ overflow: hidden;
+ display: inline-flex;
+ align-items: center;
+}
+
+.landing-ml-label {
+ padding: 0.15em 0.5em;
+ background: #00000044;
+ border-right: 1px solid #777777;
+}
+
+.landing-ml-value {
+ padding: 0.15em 0.5em;
+}
+
+/*
+ * CARD BUTTON
+ */
+
+.landing-card-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ box-sizing: border-box;
+
+ /* "Chunk" styling: chunky padding and thick borders */
+ padding: 14px 32px;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 1.1rem;
+ font-weight: 700;
+ text-decoration: none;
+ text-align: center;
+ letter-spacing: 0.5px;
+ cursor: pointer;
+
+ /* Colors & Border */
+ color: #111111 !important;
+ background-color: #ffffff;
+ border: 2px solid #111111;
+ border-radius: 0.5em;
+
+ /* Smooth transitions for hover/active states */
+ transition: all 0.2s ease-in-out;
+}
+
+@media (max-width: 600px) {
+ .landing-card-btn {
+ padding: 4px 16px !important;
+ }
+}
+
+/* Hover state */
+.landing-card-ovh:hover .landing-card-btn,
+.landing-card-ovh:active .landing-card-btn,
+.landing-card-btn:hover {
+ color: #ffffff !important;
+ background-color: #222222;
+ /* Shifts the button slightly and expands shadow for a "lifting" effect */
+ transform: translate(-2px, -2px);
+ box-shadow: 6px 6px 0px 0px #000000;
+ border-color: transparent;
+}
+
+/* Focus state for accessibility */
+.landing-card-btn:focus-visible {
+ outline: 4px solid #818cf8;
+}
+
+.hero-cards-bg {
+ transition: all 0.5s ease-in-out;
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+#foundation-sponsor-carousel {
+ mask-image: linear-gradient(
+ to right,
+ transparent 0%,
+ black 10%,
+ black 90%,
+ transparent 100%
+ );
+}