WIP checkpoint

This commit is contained in:
2026-05-20 11:28:23 -04:00
parent dd55c1edc6
commit d756b91571
21 changed files with 806 additions and 49 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+45
View File
@@ -0,0 +1,45 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
type Props = {
title: string;
intro?: string;
};
const {
title,
intro = 'This page is a placeholder and will be replaced with final approved content.',
} = Astro.props as Props;
---
<BaseLayout title={title} description={`${title} information for Swansea Airport.`}>
<section class="container prose">
<p class="eyebrow">About</p>
<h1 class="section-title">{title}</h1>
<p>{intro}</p>
<div class="cards-grid">
<article class="card">
<h3>What this page will cover</h3>
<p>
Placeholder summary content for this topic. Replace with operational guidance,
responsibilities, and current policy details.
</p>
</article>
<article class="card">
<h3>Key contacts and process</h3>
<p>
Placeholder contact details and process notes. Replace with named contacts,
response times, and escalation routes.
</p>
</article>
<article class="card">
<h3>Documents and references</h3>
<p>
Placeholder links and downloadable resources can be added here once content and
approval are complete.
</p>
</article>
</div>
</section>
</BaseLayout>
+15 -11
View File
@@ -1,7 +1,8 @@
---
import SectionHeading from './SectionHeading.astro';
import type { EventItem } from '../lib/fallback-data';
import { formatDateTime } from '../lib/format';
import { formatDate, formatTime, formatWeekday } from '../lib/format';
import { normalizeSlug } from '../lib/slug';
type Props = {
events: EventItem[];
@@ -17,22 +18,25 @@ const { events, title = 'Upcoming events', description = 'A quick scan list for
<div class="stack">
{events.length > 0 ? (
events.map((event) => {
const detailHref = event.slug ? `/events/${event.slug}/` : undefined;
const normalizedSlug = normalizeSlug(event.slug);
const detailHref = normalizedSlug ? `/events/${normalizedSlug}/` : undefined;
const summary = event.summary?.trim() || event.description;
return (
<article class="card">
<div class="split-grid" style="align-items:start;">
<div>
<p class="pill">{event.is_featured ? 'Featured' : 'Event'}</p>
<h3>{detailHref ? <a href={detailHref}>{event.title}</a> : event.title}</h3>
<p>{event.description}</p>
<article class="card event-card">
{detailHref && <a class="stretched-link" href={detailHref} aria-label={`View ${event.title}`} />}
<div class="event-layout">
<div class="event-date-block">
<p class="event-weekday">{formatWeekday(event.start_datetime)}</p>
<p class="event-date">{formatDate(event.start_datetime)}</p>
<p class="event-time">{formatTime(event.start_datetime)}</p>
</div>
<div>
<p class="meta">{formatDateTime(event.start_datetime)}</p>
<h3>{event.title}</h3>
<p>{summary}</p>
{event.location_text && <p>{event.location_text}</p>}
{detailHref && <p><a class="button primary" href={detailHref}>View event</a></p>}
{event.registration_link && (
<p><a class="button secondary" href={event.registration_link}>Register</a></p>
<p class="event-card-actions"><a class="button secondary" href={event.registration_link}>Register</a></p>
)}
</div>
</div>
+41 -7
View File
@@ -1,7 +1,6 @@
---
import SectionHeading from './SectionHeading.astro';
import type { FuelPrice } from '../lib/fallback-data';
import { formatDate } from '../lib/format';
type Props = {
fuelPrices: FuelPrice[];
@@ -16,15 +15,50 @@ function formatFuelPrice(value: unknown): string {
---
<section>
<SectionHeading eyebrow="Fuel" title="Current fuel prices" description="A simple, mobile-friendly snapshot of active prices and update times." />
<div class="cards-grid">
<SectionHeading eyebrow="Fuel" title="Current prices" description="A simple, mobile-friendly snapshot of active prices." />
<div class="cards-grid fuel-cards-grid">
{fuelPrices.map((fuel) => (
<article class="card">
<p class="pill">{fuel.fuel_type}</p>
<h3>{fuel.currency} {formatFuelPrice(fuel.price_per_litre)} / litre</h3>
<p class="muted">Updated {formatDate(fuel.last_updated)}</p>
<article class="card fuel-card">
<p class="pill fuel-type-pill">{fuel.fuel_type}</p>
<h3 class="fuel-price">{fuel.currency} {formatFuelPrice(fuel.price_per_litre)} <span>/ litre</span></h3>
{fuel.notes && <p>{fuel.notes}</p>}
</article>
))}
</div>
</section>
<style>
.fuel-cards-grid {
gap: 1.1rem;
}
.fuel-card {
border: 1px solid rgba(11, 79, 122, 0.18);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(232, 243, 253, 0.94));
}
.fuel-type-pill {
width: fit-content;
margin: 0 0 0.7rem;
padding: 0.35rem 0.85rem;
font-size: 0.83rem;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 800;
background: linear-gradient(180deg, rgba(11, 79, 122, 0.17), rgba(29, 118, 184, 0.15));
color: var(--brand);
}
.fuel-price {
margin: 0;
font-size: clamp(1.7rem, 3vw, 2.25rem);
line-height: 1.1;
color: var(--text);
}
.fuel-price span {
font-size: 0.9rem;
font-weight: 600;
color: var(--muted);
}
</style>
+28 -4
View File
@@ -22,11 +22,24 @@ const pageTitle = title ? `${title} · ${site.name}` : site.name;
<body>
<header class="topbar">
<div class="container topbar-inner">
<a class="brand brand-mobile" href="/"><img src="/images/logo-text.webp" alt="Swansea Airport" /></a>
<details class="mobile-nav">
<summary>Menu</summary>
<summary></summary>
<div class="mobile-nav-panel">
{site.navigation.map((item) => (
<a href={item.href}>{item.label}</a>
item.children ? (
<details class="mobile-subnav">
<summary>{item.label}</summary>
<div class="mobile-subnav-panel">
<a href={item.href}>Overview</a>
{item.children.map((child) => (
<a href={child.href}>{child.label}</a>
))}
</div>
</details>
) : (
<a href={item.href}>{item.label}</a>
)
))}
</div>
</details>
@@ -35,11 +48,22 @@ const pageTitle = title ? `${title} · ${site.name}` : site.name;
<nav class="navshell" aria-label="Primary">
<div class="container nav-inner">
<a class="brand" href="/">Swansea Airport</a>
<a class="brand" href="/"><img src="/images/logo-text.webp" alt="Swansea Airport" /></a>
<div class="nav-links">
{site.navigation.map((item) => (
<a href={item.href}>{item.label}</a>
item.children ? (
<div class="nav-item nav-dropdown">
<a href={item.href}>{item.label}</a>
<div class="nav-dropdown-menu">
{item.children.map((child) => (
<a href={child.href}>{child.label}</a>
))}
</div>
</div>
) : (
<a href={item.href}>{item.label}</a>
)
))}
</div>
</div>
+2
View File
@@ -19,6 +19,7 @@ export type FuelPrice = {
export type EventItem = {
title: string;
slug: string;
summary?: string;
description: string;
start_datetime: string;
end_datetime?: string;
@@ -81,6 +82,7 @@ export const fallbackEvents: EventItem[] = [
{
title: 'Airfield open day',
slug: 'airfield-open-day',
summary: 'A family-friendly open day with aircraft on display and pilot meet-and-greets.',
description: 'Example event to verify the listing and detail page flow.',
start_datetime: '2026-06-14T09:00:00Z',
end_datetime: '2026-06-14T16:00:00Z',
+21
View File
@@ -23,3 +23,24 @@ export function formatDateTime(value?: string) {
minute: '2-digit',
}).format(new Date(value));
}
export function formatWeekday(value?: string) {
if (!value) {
return 'TBC';
}
return new Intl.DateTimeFormat('en-GB', {
weekday: 'long',
}).format(new Date(value));
}
export function formatTime(value?: string) {
if (!value) {
return 'Time TBC';
}
return new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
}).format(new Date(value));
}
+15 -4
View File
@@ -6,14 +6,25 @@ export const site = {
openingHours: '7 days 0900-1600',
licensedHours: 'Friday to Sunday 0900-1700',
runwayFacts: [
'Runway 04/22 concrete 1351m x 30m licensed',
'Runway 10/28 asphalt 857m x 18m unlicensed',
'Category 1 RFFS',
'Runway 22 - 1200x45m LDA (Concrete) Code 2 (Right Hand)',
'Runway 04 - 1200x45m LDA (Concrete) Code 2 (Left Hand)',
'Runway 10 - 824x18m LDA (Asphalt) Code 1 (Right Hand)',
'Runway 28 - 794x18m LDA (Asphalt) Code 1 (Left Hand)',
'Category 1 RFFS (When Licensed)',
'Air Ground Service 119.705',
],
navigation: [
{ label: 'Home', href: '/' },
{ label: 'Visiting Pilots', href: '/visiting-pilots/' },
{ label: 'Pilot Info', href: '/pilot-info/' },
{
label: 'About',
href: '/about/',
children: [
{ label: 'Drones', href: '/about/drones/' },
{ label: 'Noise', href: '/about/noise/' },
{ label: 'Volunteering', href: '/about/volunteering/' },
],
},
{ label: 'Procedures', href: '/procedures-safety-noise-abatement/' },
{ label: 'Events', href: '/events/' },
{ label: 'News', href: '/news/' },
+8
View File
@@ -0,0 +1,8 @@
export function normalizeSlug(value?: string | null): string | null {
if (!value) return null;
const trimmed = value.trim();
if (!trimmed) return null;
return trimmed.replace(/^\/+|\/+$/g, '').toLowerCase();
}
+54
View File
@@ -0,0 +1,54 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
---
<BaseLayout title="Drones" description="Drone operating guidance and coordination process for Swansea Airport.">
<section class="container prose">
<p class="eyebrow">About</p>
<h1 class="section-title">Drones</h1>
<p>
Firstly, please be assured that Swansea Airport likes to help our drone flying colleagues
where we possibly can.
</p>
<p>
You are likely aware of the map adjacent: the purple rectangles show the Runway Protection
Zones, the purple circle is the Flight Restriction Zone (FRZ), and the red circle is the
Parachuting Protection Zone.
</p>
<p>
We ask all drone operators operating in the vicinity of the aerodrome to let us know with as
much notice as possible via email, with the following information:
</p>
<ul>
<li>Date of flight</li>
<li>Estimated take-off time</li>
<li>Estimated completion time</li>
<li>Location</li>
<li>Maximum elevation (feet above mean sea level)</li>
<li>Details of the Operator and Flyer ID</li>
</ul>
<p>
We will then reply with any operational considerations and, if required, approve or decline a
flight if it is within the FRZ or RPZs. We are not in the habit of declining, so please email
us.
</p>
<p>
After this, all we ask is that on the day, you call us in tower on
<a href="tel:01792687042">01792 687042</a> twenty minutes prior to launch, so we can give
you any operational updates and also broadcast a warning on our frequency to any aircraft.
</p>
<h2>Parachuting</h2>
<p>
Parachuting tends to take place Friday to Sunday from Spring to Winter. This can involve free
fall descents from 15,000' AMSL. We recommend avoiding these times due to the requirement to
ensure the safety of descending skydivers.
</p>
</section>
</BaseLayout>
+29
View File
@@ -0,0 +1,29 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
---
<BaseLayout title="About" description="Background and community information for Swansea Airport.">
<section class="container prose">
<p class="eyebrow">About</p>
<h1 class="section-title">About</h1>
<p>
This section provides information for local communities and visitors. Choose a topic
below to view placeholder content that will be replaced with final approved copy.
</p>
<div class="cards-grid">
<article class="card">
<h3><a href="/about/drones/">Drones</a></h3>
<p>Guidance and local operating expectations for drone use near the airport.</p>
</article>
<article class="card">
<h3><a href="/about/noise/">Noise</a></h3>
<p>Information about noise awareness, reporting, and community engagement.</p>
</article>
<article class="card">
<h3><a href="/about/volunteering/">Volunteering</a></h3>
<p>How people can contribute time and support airport activity and events.</p>
</article>
</div>
</section>
</BaseLayout>
+79
View File
@@ -0,0 +1,79 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
---
<BaseLayout title="Noise" description="Noise information, complaints process, and legal context for Swansea Airport.">
<section class="container prose">
<p class="eyebrow">About</p>
<h1 class="section-title">Noise</h1>
<p>
Swansea Airport, and its proprietors Swansea Airport Stakeholders Alliance, always look to
operate in a way to minimise any negative impact on its neighbours and the local environment
as much as we possibly can, whilst of course ensuring we can carry out our operations as an
aerodrome as effectively as possible.
</p>
<p>
The impact of noise will always be a consideration in any operational planning that takes
place. The aerodrome is also putting a register of noise complaints into place, so that we
can monitor noise matters in certain areas to ensure we have the maximum knowledge available
when making decisions.
</p>
<p>
The aerodrome authority will always monitor the situation, and attempt to advise based pilots
of any sensitive areas.
</p>
<p>
With the above said, it should be noted that we are not an Air Traffic Control authority, and
that the conduct of any flight is solely under the authority of the commander of the aircraft.
Neither Swansea Airport, nor Swansea Airport Stakeholders Alliance, has authority to direct,
control, restrict, or otherwise interfere with the chosen legal actions or decisions of the
aircraft commander.
</p>
<p>
It is also worth noting that s.79(6) of the Environmental Protection Act 1990 specifically
exempts aircraft noise from the general noise nuisance controls which exist under that
legislation, and s.76 of the Civil Aviation Act 1982 exempts an aircraft that was being flown
in accordance with the regulations governing aircraft flight from prosecution for trespass or
nuisance.
</p>
<p>
If you believe that a flight was conducted contrary to those regulations, then a complaint can
be filed online with the Civil Aviation Authority, who has the power to investigate (search
for CAA Form FCS1520).
</p>
<h2>Contact and complaints</h2>
<p>
If you would like to discuss noise at Swansea Airport with us, please feel free to email
<a href="mailto:tower@swansea-airport.wales">tower@swansea-airport.wales</a>. Please also
use this email address if you would like to log a noise complaint.
</p>
<p>
Please be advised we cannot discuss noise issues or complaints over the telephone initially,
and all such matters have to be raised by email initially.
</p>
<p>
When sending an email, please set the subject to <strong>NOISE COMPLAINT</strong> and try to
include the following:
</p>
<ul>
<li>The date of the event</li>
<li>The time of the event</li>
<li>The location affected</li>
<li>If possible, the aircraft registration (often found on the underside of the wing)</li>
<li>
If registration is not possible, a description of the aircraft (visible colours, high wing
or low wing aircraft, number of engines, etc.)
</li>
</ul>
</section>
</BaseLayout>
+67
View File
@@ -0,0 +1,67 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
---
<BaseLayout title="Volunteering" description="Volunteering vacancies and opportunities at Swansea Airport.">
<section class="container prose">
<p class="eyebrow">About</p>
<h1 class="section-title">Volunteering Vacancies at Swansea Airport</h1>
<p>
Swansea Airport is manned and run by a mix of volunteers and staff. Volunteering can be
incredibly beneficial in many ways. Here are several key reasons why volunteering at Swansea
Airport can have a positive impact.
</p>
<h2>Skill Development</h2>
<ul>
<li>
<strong>Transferable skills:</strong> Volunteering allows you to develop skills that are
valuable in the workplace, such as communication, teamwork, leadership, time management,
problem solving, and organisation.
</li>
<li>
<strong>Industry-specific experience:</strong> Many of our volunteer roles can provide
hands-on experience in the aviation field, which can be hard to get initially.
</li>
</ul>
<h2>Networking Opportunities</h2>
<ul>
<li>
<strong>Building connections:</strong> Volunteering can expose you to a diverse collection
of people, including professionals, enthusiasts, and community leaders. These connections
can lead to job opportunities, mentorship, and highly valuable professional connections.
</li>
<li>
<strong>Expanding your circle:</strong> Networking with others in your field can give you
leading insights into job openings and industry trends, potentially giving you a
competitive edge in your future employment searches.
</li>
</ul>
<h2>CV Enhancement</h2>
<ul>
<li>
<strong>Demonstrates commitment and initiative:</strong> Employers value candidates who
show commitment to helping others and contributing to society.
</li>
<li>
<strong>Adds diversity to your experience:</strong> Volunteering can fill gaps in your CV,
especially if you are just starting out or transitioning to a new field.
</li>
</ul>
<p>
Those are just a few reasons. If you are interested in expanding your horizons and
volunteering at Swansea Airport, please pop up and ask to speak to somebody, or register your
interest on our dedicated volunteering portal.
</p>
<p>
<a href="https://webcollect.org.uk/sav" target="_blank" rel="noopener noreferrer">
https://webcollect.org.uk/sav
</a>
</p>
</section>
</BaseLayout>
+11 -2
View File
@@ -2,6 +2,7 @@
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getEvents } from '../../lib/directus';
import { formatDateTime } from '../../lib/format';
import { normalizeSlug } from '../../lib/slug';
type EventItem = Awaited<ReturnType<typeof getEvents>>[number];
@@ -32,7 +33,15 @@ function resolveEventImageSource(realimage: EventItem['realimage']): string | nu
export async function getStaticPaths() {
const events = await getEvents();
return events.map((item) => ({ params: { slug: item.slug }, props: { item } }));
const paths = new Map<string, { params: { slug: string }; props: { item: EventItem } }>();
for (const item of events) {
const slug = normalizeSlug(item.slug);
if (!slug || paths.has(slug)) continue;
paths.set(slug, { params: { slug }, props: { item } });
}
return Array.from(paths.values());
}
const { item } = Astro.props as { item: EventItem };
@@ -45,7 +54,7 @@ const imageAlt = item.title;
<p class="meta">{formatDateTime(item.start_datetime)}</p>
<h1 class="section-title">{item.title}</h1>
{imageSrc && <p><img src={imageSrc} alt={imageAlt} loading="lazy" /></p>}
<p>{item.description}</p>
{item.description && <p class="text-multiline">{item.description}</p>}
{item.location_text && <p><strong>Location:</strong> {item.location_text}</p>}
{item.registration_link && <p><a class="button primary" href={item.registration_link}>Register</a></p>}
</article>
+1 -1
View File
@@ -8,6 +8,6 @@ const events = (await getEvents()).sort((left, right) => new Date(left.start_dat
<BaseLayout title="Events" description="Airport events and flying opportunities.">
<div class="container">
<EventsList events={events} title="Events listing" description="Scannable listings for public and operational events." />
<EventsList events={events} title="Upcoming Events" description="What's coming up at the airport" />
</div>
</BaseLayout>
+5 -2
View File
@@ -50,10 +50,13 @@ const businessPromos = [
---
<BaseLayout title="Home" description="Fast, clear airfield information for pilots and visitors.">
<div class="banner-image">
<img src="/images/banner.png" alt="Swansea Airport banner" loading="eager" />
</div>
<section class="hero">
<div class="container hero-stack">
<div class="hero-panel">
<img class="hero-logo" src="/images/swansea.webp" alt="Swansea Airport logo" loading="eager" />
<p class="eyebrow">Operational website</p>
<h1 class="hero-title">Welcome to Swansea Airport - Fairwood Common</h1>
<p class="hero-copy">
@@ -63,7 +66,7 @@ const businessPromos = [
There are a whole host of aviation activities at Swansea Airport. If you're looking to learn to fly, or charter an aircraft, Swansea is more convenient and more cost effective than other Welsh and West of England airports. No queues and no expensive handling fees.
</p>
<div class="cta-row">
<a class="button primary" href="/visiting-pilots/">Visiting pilots</a>
<a class="button primary" href="/pilot-info/">Pilot Info</a>
<a class="button secondary" href="/procedures-safety-noise-abatement/">Procedures and safety</a>
</div>
</div>
+91
View File
@@ -0,0 +1,91 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import FuelPricesWidget from '../components/FuelPricesWidget.astro';
import { getFuelPrices } from '../lib/directus';
import { site } from '../lib/site';
const runwayPattern = /^Runway\s+(\d+)\s+-\s+([^\s]+)\s+LDA\s+\(([^)]+)\)\s+Code\s+(\d+)\s+\(([^)]+)\)$/;
const runwayRows = site.runwayFacts
.map((fact) => {
const match = fact.match(runwayPattern);
if (!match) return null;
const [, runway, lda, surface, code, circuits] = match;
return { runway, lda, surface, code, circuits };
})
.filter((row): row is { runway: string; lda: string; surface: string; code: string; circuits: string } => row !== null);
const otherFacts = site.runwayFacts.filter((fact) => !fact.match(runwayPattern));
const fuelPrices = await getFuelPrices();
---
<BaseLayout title="Pilot Info" description="Essential information for pilots planning a visit to Swansea Airport.">
<div class="container prose">
<p class="eyebrow">Pilot info</p>
<h1 class="section-title">Communications</h1>
<p>
Swansea Airport operates a Air / Ground Communication Service, callsign Swansea Radio on 119.705. This service is staffed by volunteers and may not be operational at all times.
</p>
<p>
When the Tower is unavailable, this will be NOTAMed and blind calls to Swansea Traffic shall be made on the Air / Ground frequency
</p>
<h2>Runway Information</h2>
<div class="runway-table-wrap">
<table class="runway-facts-table">
<thead>
<tr>
<th scope="col">Runway</th>
<th scope="col">LDA</th>
<th scope="col">Surface</th>
<th scope="col">Code</th>
<th scope="col">Circuits</th>
</tr>
</thead>
<tbody>
{runwayRows.map((runway) => (
<tr>
<td>{runway.runway}</td>
<td>{runway.lda}</td>
<td>{runway.surface}</td>
<td>{runway.code}</td>
<td>{runway.circuits}</td>
</tr>
))}
</tbody>
</table>
</div>
{otherFacts.length > 0 && (
<ul>
{otherFacts.map((fact) => <li>{fact}</li>)}
</ul>
)}
<p>Circuits at 1000ft agl. Overhead joins and/or dead-side flying not permitted when Parachuting is active.
</p>
<FuelPricesWidget fuelPrices={fuelPrices} />
<h2>Arrival essentials</h2>
<div class="cards-grid">
<article class="card">
<h3>PPR</h3>
<p>Pre-landing fogging is presented prominently here and can be linked to the relevant Directus content or booking workflow.</p>
</article>
<article class="card">
<h3>Book out</h3>
<p>Departure procedures and any required outbound reporting remain in the same controlled page structure.</p>
</article>
<article class="card">
<h3>Fuel and services</h3>
<p>Fuel prices are shown on the homepage and can be reused here with the same data source.</p>
</article>
</div>
</div>
</BaseLayout>
+59 -8
View File
@@ -1,20 +1,71 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { site } from '../lib/site';
const runwayPattern = /^Runway\s+(\d+)\s+-\s+([^\s]+)\s+LDA\s+\(([^)]+)\)\s+Code\s+(\d+)\s+\(([^)]+)\)$/;
const runwayRows = site.runwayFacts
.map((fact) => {
const match = fact.match(runwayPattern);
if (!match) return null;
const [, runway, lda, surface, code, circuits] = match;
return { runway, lda, surface, code, circuits };
})
.filter((row): row is { runway: string; lda: string; surface: string; code: string; circuits: string } => row !== null);
const otherFacts = site.runwayFacts.filter((fact) => !fact.match(runwayPattern));
---
<BaseLayout title="Visiting Pilots" description="Essential information for pilots planning a visit to Swansea Airport.">
<BaseLayout title="Pilot Info" description="Essential information for pilots planning a visit to Swansea Airport.">
<div class="container prose">
<p class="eyebrow">Visiting pilots</p>
<h1 class="section-title">Essential information for arriving aircraft</h1>
<p class="eyebrow">Pilot info</p>
<h1 class="section-title">Communications</h1>
<p>
This page is structured for quick pre-flight checks. It keeps operational details in fixed Astro components and leaves content updates to Directus.
Swansea Airport operates a Air / Ground Communication Service, callsign Swansea Radio on 119.705. This service is staffed by volunteers and may not be operational at all times.
</p>
<p>
When the Tower is unavailable, this will be NOTAMed and blind calls to Swansea Traffic shall be made on the Air / Ground frequency
</p>
<h2>Airport facts</h2>
<ul>
{site.runwayFacts.map((fact) => <li>{fact}</li>)}
</ul>
<h2>Runway Information</h2>
<div class="runway-table-wrap">
<table class="runway-facts-table">
<thead>
<tr>
<th scope="col">Runway</th>
<th scope="col">LDA</th>
<th scope="col">Surface</th>
<th scope="col">Code</th>
<th scope="col">Circuits</th>
</tr>
</thead>
<tbody>
{runwayRows.map((runway) => (
<tr>
<td>{runway.runway}</td>
<td>{runway.lda}</td>
<td>{runway.surface}</td>
<td>{runway.code}</td>
<td>{runway.circuits}</td>
</tr>
))}
</tbody>
</table>
</div>
{otherFacts.length > 0 && (
<ul>
{otherFacts.map((fact) => <li>{fact}</li>)}
</ul>
)}
<p>Circuits at 1000ft agl. Overhead joins not permitted when Parachuting is active.
</p>
<h2>Arrival essentials</h2>
<div class="cards-grid">
+235 -10
View File
@@ -105,6 +105,19 @@ img {
font-weight: 700;
letter-spacing: 0.02em;
text-decoration: none;
display: flex;
align-items: center;
height: auto;
}
.brand img {
max-height: 2.5rem;
width: auto;
display: block;
}
.brand-mobile {
display: none;
}
.nav-links {
@@ -119,6 +132,49 @@ img {
text-decoration: none;
}
.nav-item {
position: relative;
padding-bottom: 0.5rem;
}
.nav-dropdown-menu {
position: absolute;
top: calc(100% + 0.5rem);
left: 50%;
transform: translateX(-50%);
min-width: 11rem;
display: grid;
gap: 0.2rem;
padding: 0.45rem;
border: 1px solid var(--line);
border-radius: 0.8rem;
background: rgba(255, 255, 255, 0.97);
box-shadow: var(--shadow);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.18s ease;
z-index: 24;
margin-top: -0.5rem;
}
.nav-dropdown-menu a {
padding: 0.45rem 0.6rem;
border-radius: 0.6rem;
color: var(--text);
}
.nav-dropdown-menu a:hover {
background: var(--brand-soft);
}
.nav-dropdown:hover .nav-dropdown-menu,
.nav-dropdown:focus-within .nav-dropdown-menu {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.nav-links a:hover,
.mobile-nav-panel a:hover {
color: var(--brand);
@@ -134,15 +190,16 @@ img {
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
justify-content: center;
min-height: 2.75rem;
padding: 0.7rem 1rem;
min-width: 2.75rem;
padding: 0;
border: 1px solid var(--line);
border-radius: 999px;
border-radius: 0.7rem;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(228, 240, 252, 0.95));
color: var(--brand);
font-weight: 800;
font-size: 1.4rem;
cursor: pointer;
box-shadow: var(--shadow);
}
@@ -151,14 +208,14 @@ img {
display: none;
}
.mobile-nav summary::after {
content: '';
font-size: 0.9rem;
color: var(--brand-2);
.mobile-nav summary::before {
content: '';
line-height: 1;
}
.mobile-nav[open] summary::after {
content: '';
.mobile-nav[open] summary::before {
content: '';
font-size: 1.2rem;
}
.mobile-nav-panel {
@@ -185,10 +242,52 @@ img {
font-weight: 700;
}
.mobile-subnav {
border-radius: 0.75rem;
}
.mobile-subnav summary {
list-style: none;
padding: 0.7rem 0.8rem;
border-radius: 0.75rem;
font-weight: 700;
cursor: pointer;
}
.mobile-subnav summary::-webkit-details-marker {
display: none;
}
.mobile-subnav-panel {
display: grid;
gap: 0.2rem;
margin: 0 0.2rem 0.35rem;
padding: 0.35rem;
border: 1px solid var(--line);
border-radius: 0.7rem;
background: rgba(255, 255, 255, 0.7);
}
main {
padding-block: 1.5rem 4rem;
}
.banner-image {
width: 100%;
display: block;
overflow: hidden;
margin: 0;
background: rgba(255, 255, 255, 0.4);
}
.banner-image img {
width: 100%;
height: auto;
display: block;
max-height: 12rem;
object-fit: cover;
}
section {
padding-block: 1rem;
}
@@ -502,6 +601,64 @@ section {
margin-bottom: 0;
}
.event-card {
position: relative;
}
.stretched-link {
position: absolute;
inset: 0;
z-index: 1;
}
.event-card-actions {
position: relative;
z-index: 2;
}
.event-layout {
display: grid;
grid-template-columns: minmax(10rem, 11.5rem) minmax(0, 1fr);
gap: 1rem;
align-items: start;
}
.event-date-block {
padding: 0.85rem 0.95rem;
border: 1px solid rgba(16, 34, 51, 0.12);
border-radius: var(--radius-sm);
background: linear-gradient(180deg, rgba(11, 79, 122, 0.1), rgba(29, 118, 184, 0.06));
}
.event-weekday,
.event-date,
.event-time {
margin: 0;
}
.event-weekday {
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--brand-2);
font-size: 0.73rem;
font-weight: 800;
}
.event-date {
margin-top: 0.3rem;
font-family: 'Fraunces', Georgia, serif;
font-size: 1.2rem;
font-weight: 700;
line-height: 1.2;
}
.event-time {
margin-top: 0.35rem;
color: var(--muted);
font-size: 0.9rem;
font-weight: 700;
}
.split-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -540,18 +697,74 @@ section {
max-width: 72ch;
}
.runway-table-wrap {
width: 100%;
overflow-x: auto;
margin: 1rem auto 1.25rem;
}
.runway-facts-table {
width: min(100%, 46rem);
margin-inline: auto;
border-collapse: collapse;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow: hidden;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 12px 24px rgba(16, 34, 51, 0.08);
}
.runway-facts-table thead {
background: linear-gradient(180deg, rgba(11, 79, 122, 0.14), rgba(29, 118, 184, 0.09));
}
.runway-facts-table th,
.runway-facts-table td {
padding: 0.7rem 0.85rem;
border-bottom: 1px solid var(--line);
text-align: left;
white-space: nowrap;
}
.runway-facts-table th {
font-size: 0.83rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--brand);
}
.runway-facts-table tbody tr:nth-child(even) {
background: rgba(255, 255, 255, 0.64);
}
.runway-facts-table tbody tr:hover {
background: var(--brand-soft);
}
.runway-facts-table tbody tr:last-child td {
border-bottom: 0;
}
.prose h2,
.prose h3 {
font-family: 'Manrope', system-ui, sans-serif;
font-weight: 800;
}
.text-multiline {
white-space: pre-line;
}
.stack {
display: grid;
gap: 1rem;
}
@media (max-width: 860px) {
.event-layout {
grid-template-columns: 1fr;
}
.split-grid,
.footer-grid,
.operational-grid {
@@ -598,4 +811,16 @@ section {
.mobile-nav {
display: block;
}
.brand-mobile {
display: flex;
}
.brand-mobile img {
max-height: 2rem;
}
.runway-facts-table {
min-width: 38rem;
}
}