Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@
0 12px 28px -16px rgba(0, 0, 0, 0.8)
);

/*
* Observatory hero. The panel is deliberately single-look: navy in both
* themes, so these are constants rather than light-dark() pairs. Values
* borrow the dark theme's palette so the hero and dark mode agree.
*/
--hero-bg: #071b2e;
--hero-accent: #8fa8ff;
--hero-cta: #4d67e8;
--hero-trust: #65d2b4;
--hero-amended: #efb65e;
--hero-new: #6693ff;

/* Motion */
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
Expand Down
13 changes: 13 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,25 @@ export default async function HomePage() {
development.verification.status === 'verified',
)
.slice(0, 6);
// Anchored to the data's own currency rather than the wall clock, so the
// figure is deterministic for a given collection state (and satisfies the
// render-purity rule).
const dataCurrentAt = meta.lastHealthyAt ?? meta.lastCollectedAt;
const weekAgo = dataCurrentAt
? new Date(dataCurrentAt).getTime() - 7 * 24 * 60 * 60 * 1000
: Number.POSITIVE_INFINITY;
const weeklyDevelopmentCount = allDevelopments.filter(
(development) =>
development.status !== 'dismissed' &&
new Date(development.detectedAt).getTime() >= weekAgo,
).length;

return (
<PolicyBrowser
policies={policies}
developments={developments}
developmentCount={allDevelopments.filter((development) => development.status !== 'dismissed').length}
weeklyDevelopmentCount={weeklyDevelopmentCount}
lastCollectedAt={meta.lastCollectedAt}
lastHealthyAt={meta.lastHealthyAt}
lastReviewedAt={meta.lastReviewedAt}
Expand Down
75 changes: 48 additions & 27 deletions src/components/policy-browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ interface PolicyBrowserProps {
policies: Policy[];
developments: Development[];
developmentCount: number;
/** Developments first detected in the last seven days, dismissed excluded. */
weeklyDevelopmentCount: number;
lastCollectedAt: string | null;
lastHealthyAt: string | null;
lastReviewedAt: string | null;
Expand Down Expand Up @@ -138,6 +140,7 @@ export function PolicyBrowser({
policies,
developments,
developmentCount,
weeklyDevelopmentCount,
lastCollectedAt,
lastHealthyAt,
lastReviewedAt,
Expand Down Expand Up @@ -260,18 +263,18 @@ export function PolicyBrowser({

return (
<div>
<section className="relative overflow-hidden bg-[#071b2e] text-white">
<section className="relative overflow-hidden bg-[var(--hero-bg)] text-white">
<div className="container relative mx-auto grid min-h-[38rem] gap-12 px-4 sm:px-6 md:grid-cols-[0.72fr_1.28fr] md:gap-0 lg:px-8">
<div className="reveal flex max-w-[34rem] flex-col justify-center py-12 pr-0 md:py-10 md:pr-8 lg:pr-10">
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-[#82a7ff]">
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-[var(--hero-accent)]">
The policy observatory
</p>
<h1 className="mt-5 max-w-[29rem] font-sans text-[clamp(2.7rem,4.25vw,4.25rem)] font-semibold leading-[1.03] tracking-[-0.035em] text-white">
See Australian AI policy as it changes.
</h1>
<p className="mt-5 max-w-[29rem] text-[14px] leading-6 text-white/64">
Policai tracks official AI policy across Australia in real time and
links every record to its source.
Policai tracks official AI policy across Australia, checked daily,
with every record linked to its source.
</p>

<form
Expand All @@ -283,7 +286,7 @@ export function PolicyBrowser({
<label className="sr-only" htmlFor="observatory-search">
Search the Australian AI policy register
</label>
<div className="group flex max-w-[31rem] items-center border border-white/30 bg-transparent transition-colors focus-within:border-[#82a7ff] focus-within:bg-white/[0.035]">
<div className="group flex max-w-[31rem] items-center border border-white/30 bg-transparent transition-colors focus-within:border-[var(--hero-accent)] focus-within:bg-white/[0.035]">
<Search className="ml-4 h-[18px] w-[18px] shrink-0 text-white/45" strokeWidth={1.7} />
<input
id="observatory-search"
Expand All @@ -300,34 +303,52 @@ export function PolicyBrowser({
<div className="mt-5 flex flex-wrap items-center gap-x-6 gap-y-3 text-xs">
<button
type="submit"
className="group inline-flex h-12 items-center gap-4 rounded-[2px] bg-gradient-to-r from-[#435ee6] to-[#6178ea] px-6 font-semibold text-white transition-[filter] hover:brightness-110"
className="group inline-flex h-12 items-center gap-4 rounded-[2px] bg-[var(--hero-cta)] px-6 font-semibold text-white transition-[filter] hover:brightness-110"
>
Search the register
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" strokeWidth={2} />
</button>
<Link href="/developments" className="group inline-flex items-center gap-3 font-medium text-[#9bb6ff] hover:text-white">
<Link href="/developments" className="group inline-flex items-center gap-3 font-medium text-[var(--hero-accent)] hover:text-white">
View developments
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</Link>
</div>
</form>

{/* Each figure is a doorway, not a decoration. */}
<div className="mt-9 grid max-w-[31rem] grid-cols-4">
{[
[policies.length, 'verified policies'],
[distinctJurisdictions.size, 'jurisdictions'],
[developmentCount, 'developments'],
[automaticSourceCount, 'sources monitored'],
].map(([value, label], index) => (
<div key={label} className={cn('pr-3', index > 0 && 'border-l border-white/20 pl-5')}>
<strong className="block text-[1.55rem] font-medium leading-none text-white">
{value}
</strong>
<span className="mt-2 block text-[10px] leading-4 text-white/55">
{label}
</span>
</div>
))}
{(
[
{ value: policies.length, label: 'verified policies', href: '#policy-register', delta: null },
{ value: distinctJurisdictions.size, label: 'jurisdictions', href: '/map', delta: null },
{ value: developmentCount, label: 'developments', href: '/developments', delta: weeklyDevelopmentCount },
{ value: automaticSourceCount, label: 'sources monitored', href: '/methodology', delta: null },
] as const
).map((stat, index) => {
const StatLink = stat.href.startsWith('#') ? 'a' : Link;
return (
<StatLink
key={stat.label}
href={stat.href}
className={cn(
'group/stat block pr-3 transition-colors duration-[var(--dur-fast)]',
index > 0 && 'border-l border-white/20 pl-5',
)}
>
<strong className="block text-[1.55rem] font-medium leading-none text-white transition-colors duration-[var(--dur-fast)] group-hover/stat:text-[var(--hero-accent)]">
{stat.value}
</strong>
<span className="mt-2 block text-[10px] leading-4 text-white/55 transition-colors duration-[var(--dur-fast)] group-hover/stat:text-white/80">
{stat.label}
</span>
{stat.delta ? (
<span className="mt-1 block text-[10px] leading-4 text-[var(--hero-trust)]">
+{stat.delta} this week
</span>
) : null}
</StatLink>
);
})}
</div>
</div>

Expand All @@ -337,13 +358,13 @@ export function PolicyBrowser({
</div>
</section>

<section className="border-y border-white/16 bg-[#071b2e] text-white">
<section className="border-y border-white/16 bg-[var(--hero-bg)] text-white">
<div className="container mx-auto px-4 py-8 sm:px-6 lg:px-8">
<div className="flex items-center justify-between gap-6 pb-4">
<h2 className="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-[#82a7ff]">
<h2 className="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-[var(--hero-accent)]">
What changed recently
</h2>
<Link href="/developments" className="group hidden items-center gap-3 text-xs font-medium text-[#9bb6ff] hover:text-white sm:inline-flex">
<Link href="/developments" className="group hidden items-center gap-3 text-xs font-medium text-[var(--hero-accent)] hover:text-white sm:inline-flex">
View all developments
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-1" />
</Link>
Expand All @@ -367,7 +388,7 @@ export function PolicyBrowser({
href={development.url}
target="_blank"
rel="noopener noreferrer"
className="group mt-3 inline-flex max-w-[23rem] items-start gap-2 text-[15px] font-semibold leading-6 hover:text-[#9bb6ff]"
className="group mt-3 inline-flex max-w-[23rem] items-start gap-2 text-[15px] font-semibold leading-6 hover:text-[var(--hero-accent)]"
>
{development.title}
<ArrowUpRight className="mt-1 h-4 w-4 shrink-0 opacity-45 transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:opacity-100" />
Expand All @@ -378,7 +399,7 @@ export function PolicyBrowser({
</p>
) : null}
<p className="mt-4 flex items-center gap-2 text-[11px] text-white/60">
<CheckCircle2 className="h-3.5 w-3.5 text-[#77dcc2]" fill="currentColor" />
<CheckCircle2 className="h-3.5 w-3.5 text-[var(--hero-trust)]" fill="currentColor" />
{getJurisdictionName(development.jurisdiction)}
<span className="text-white/24">|</span>
Verified source
Expand Down
18 changes: 9 additions & 9 deletions src/components/policy-observatory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ const jurisdictionLabel: Record<Jurisdiction, string> = {
};

const legendItems = [
{ label: 'Active', className: 'bg-[#69c5a3]' },
{ label: 'Amended', className: 'bg-[#efb65e]' },
{ label: 'New (last 30 days)', className: 'bg-[#6693ff]' },
{ label: 'Active', className: 'bg-[var(--hero-trust)]' },
{ label: 'Amended', className: 'bg-[var(--hero-amended)]' },
{ label: 'New (last 30 days)', className: 'bg-[var(--hero-new)]' },
{ label: 'Planned / Draft', className: 'border border-white/65 bg-transparent' },
{ label: 'Inactive', className: 'bg-white/28' },
];
Expand All @@ -43,11 +43,11 @@ function formatAxisDate(date: Date): string {
function pointClass(policy: Policy, date: Date, currentDate: Date): string {
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
if (currentDate.getTime() - date.getTime() <= thirtyDays && date <= currentDate) {
return 'bg-[#6693ff]';
return 'bg-[var(--hero-new)]';
}
if (policy.status === 'proposed') return 'border border-white/65 bg-[#071b2e]';
if (policy.status === 'amended') return 'bg-[#efb65e]';
if (policy.status === 'active') return 'bg-[#69c5a3]';
if (policy.status === 'proposed') return 'border border-white/65 bg-[var(--hero-bg)]';
if (policy.status === 'amended') return 'bg-[var(--hero-amended)]';
if (policy.status === 'active') return 'bg-[var(--hero-trust)]';
return 'bg-white/28';
}

Expand Down Expand Up @@ -118,7 +118,7 @@ export function PolicyObservatory({
key={jurisdiction}
className="grid h-[3.15rem] grid-cols-[9.5rem_1fr] items-center"
>
<span className="pr-4 text-[12px] font-medium text-[#82a7ff]">
<span className="pr-4 text-[12px] font-medium text-[var(--hero-accent)]">
{jurisdictionLabel[jurisdiction]}
<span className="ml-2 font-mono text-[10px] text-white/48">
{jurisdictionPolicies.length}
Expand All @@ -135,7 +135,7 @@ export function PolicyObservatory({
title={`${policy.title} — ${formatAxisDate(date)}`}
aria-label={`Open ${policy.title}`}
className={cn(
'absolute z-20 block h-[9px] w-[9px] -translate-x-1/2 -translate-y-1/2 rounded-full ring-1 ring-[#071b2e] transition duration-150 hover:z-30 hover:scale-[1.75] focus:z-30 focus:scale-[1.75]',
'absolute z-20 block h-[9px] w-[9px] -translate-x-1/2 -translate-y-1/2 rounded-full ring-1 ring-[var(--hero-bg)] transition duration-150 hover:z-30 hover:scale-[1.75] focus:z-30 focus:scale-[1.75]',
pointClass(policy, date, currentDate),
)}
style={{
Expand Down
Loading