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"])) { - "> + <?php echo $title ?> @@ -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["title"]} -

-
-
- {$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="id) ?>" href="href) ?>" class="vgrid-card landing-card-ovh "> +
    +
    + image) { ?>Graphic of <?= $card->title ?> +
    title) ?>
    +
    + +
    about) ?>
    + + href) { ?> +
    + 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'); + + ?> +
    +
    + · + Changelog · Upgrading +
    +
    +
    +
    + Bugfixes: + +
    +
    + Security: + +
    +
    +
    +
    + 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(); +?> +
+ + +
+
+
+
+
+ + PHP Elephant logo +
+ + + +
+
+
+
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.
+
+
+
+ +
+
+
'landing-cc-card landing-cc-card-alt']) ?>
+
+
+
+
+ +
+
+
+ + + + + + + + + + + +
+
+
+ php +
+ +
+ + +
+
+
Major Features
+
+ +
+
+
+
+ +
+
+
+
+
+ + +
+
+ +
+
+
+ PHP Foundation Logo +
+
+ The PHP Foundation is a collective of people and organizations, united in the mission to ensure the long-term prosperity of the PHP language. +

+
+ + Learn About the PHP Foundation +  ·  + + Donate Via Open Collective +  ·  + + Donate Via GitHub +
+
+ +
+
The PHP Foundation is grateful for our many sponsors, including:
+ + + + +
+ +
+
+ + +
+
+
+
Community
+
+
'landing-cc-card landing-cc-card-grey']); ?>
+
+
+ +
Events & Conferences
+
+
'landing-cc-card landing-cc-card-grey']) ?>
+
+
+
+
+ +
+
+
+
+ Composer Logo +
+
+
+ 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
+
+ '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); -
-
- -
+
+ +
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)); -
-
- -
+
+ +
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)); -
-
- -
+
+ +
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)); -
-
- -
+
+ +
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)); -
-
- -
+ +
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
+
+
+
+
Editor
+
+ +
+
+ +
+
+
+
Output
+
+
+
+
+
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.
+ +
+
+
+
+ +
+
+
+
+
+
+
Debug
+
+
+
+ + +
+
+
+
+
+
+
+
+ + + + '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% + ); +}