Waving Hands: a browser duel after Bartle's 1977 game

SvelteKit 5 single-player version against a heuristic bot. The engine
resolves all forty spells simultaneously as the rules describe, the
ledger of gestures is the interface, and the duel is saved in the
browser. Tester feedback rounds added per-hand planning, threat
evidence, a result strip with one-time rule explanations, a phone
layout and a structured rules page. Deploys as a static site behind
Caddy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-09-22 15:33:29 -04:00
co-authored by Claude Fable 5.1
commit 865555e911
38 changed files with 8112 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children()}
+4
View File
@@ -0,0 +1,4 @@
// The duel lives in the browser and is saved to local storage, so nothing is rendered on the
// server. Each route is prerendered as an empty shell that the client fills in.
export const ssr = false;
export const prerender = true;
+541
View File
@@ -0,0 +1,541 @@
<script lang="ts">
import Chronicle from '$lib/components/Chronicle.svelte';
import HandPicker from '$lib/components/HandPicker.svelte';
import Ledger from '$lib/components/Ledger.svelte';
import MobileBar from '$lib/components/MobileBar.svelte';
import SpellSheet from '$lib/components/SpellSheet.svelte';
import TurnSummary from '$lib/components/TurnSummary.svelte';
import WizardStatus from '$lib/components/WizardStatus.svelte';
import { duel, FOE, YOU, castKey, tokensText } from '$lib/game/duel.svelte';
import { GESTURE_SHORT, glyph } from '$lib/game/glyphs';
import { MAGIC_GESTURES } from '$lib/game/spells';
let confirmingNew = $state(false);
function startNew() {
if (duel.inProgress && !confirmingNew) {
confirmingNew = true;
return;
}
confirmingNew = false;
duel.newGame();
}
// Save the duel whenever anything about it changes, and follow the window width.
$effect(() => {
duel.persist();
});
$effect(() => {
const query = window.matchMedia('(max-width: 860px)');
const apply = () => (duel.compact = query.matches);
apply();
query.addEventListener('change', apply);
return () => query.removeEventListener('change', apply);
});
const foeMonsters = $derived(duel.state.monsters.filter((m) => m.owner === FOE));
const outcome = $derived(duel.state.over);
const won = $derived(outcome?.winner === YOU);
</script>
<svelte:head>
<title>Waving Hands</title>
<meta name="description" content="A duel of gestures against a rival wizard, after Richard Bartle's 1977 pencil-and-paper game." />
</svelte:head>
<header class="masthead">
<div>
<h1>Waving Hands</h1>
{#if duel.state.turn === 0 && !duel.timeStopped}
<p class="standfirst">You are <em>{duel.you.name}</em>, duelling <em>{duel.foe.name}</em>. Each turn, both wizards choose one gesture per hand and reveal them together. The right run of gestures on one hand is a spell.</p>
{:else}
<p class="standfirst">You are <em>{duel.you.name}</em>, duelling <em>{duel.foe.name}</em>.</p>
{/if}
</div>
<nav class="actions">
<a class="quiet" href="/rules">Rules</a>
<button type="button" class="quiet" onclick={startNew}>New duel</button>
</nav>
</header>
{#if confirmingNew}
<div class="confirm" role="alertdialog" aria-labelledby="confirm-heading">
<p id="confirm-heading">Abandon the duel against {duel.foe.name}? Turn {duel.turnNumber} will be lost.</p>
<div class="choices">
<button type="button" class="reveal small" onclick={startNew}>Start a new duel</button>
<button type="button" class="quiet" onclick={() => (confirmingNew = false)}>Keep playing</button>
</div>
</div>
{/if}
<main class="board">
<section class="play">
<Ledger {duel} />
<TurnSummary {duel} />
{#if outcome}
<div class="verdict" class:won>
<h2>{won ? 'You win.' : outcome.winner === null ? 'A draw.' : 'You lose.'}</h2>
<p>{outcome.reason}</p>
<button type="button" class="reveal" onclick={() => duel.newGame()}>Duel again</button>
</div>
{:else}
<section class="move" aria-labelledby="move-heading">
{#if duel.timeStopped}
<h2 id="move-heading">Time stands still. Take your extra turn.</h2>
<p class="threats"><span class="muted">{duel.foe.name} cannot move, see this, or resist anything you do. Last turn's enchantments do not bind you.</span></p>
{:else}
<h2 id="move-heading">Turn {duel.turnNumber}: write your gestures{#if duel.hasted}, twice{/if}</h2>
{/if}
{#if duel.timeStopped}
<!-- nobody else moves -->
{:else if duel.threats.length}
<div class="threats">
{#if duel.threatsNow.length}
<p>Their hands could finish this turn:</p>
<ul>
{#each duel.threatsNow as t (t.hand + t.spell.id)}
{@const active = duel.highlight?.hand === t.hand && duel.highlight.from === t.from}
<li>
<button type="button" class="threat soon" class:active aria-pressed={active} onclick={() => duel.toggleHighlight(t)}>
<span class="thand">{t.hand}</span>
<span class="tseq">{tokensText(t.done)}<strong>{tokensText(t.remaining)}</strong></span>
<span class="tname">{t.spell.name}</span>
</button>
</li>
{/each}
</ul>
{/if}
{#if duel.threatsNext.length}
<details class="next" open={!duel.compact}>
<summary class="muted">Two gestures away ({duel.threatsNext.length})</summary>
<ul>
{#each duel.threatsNext as t (t.hand + t.spell.id)}
{@const active = duel.highlight?.hand === t.hand && duel.highlight.from === t.from && duel.highlight.to === t.to}
<li>
<button type="button" class="threat" class:active aria-pressed={active} onclick={() => duel.toggleHighlight(t)}>
<span class="thand">{t.hand}</span>
<span class="tseq">{tokensText(t.done)}{tokensText(t.remaining)}</span>
<span class="tname">{t.spell.name}</span>
</button>
</li>
{/each}
</ul>
</details>
{/if}
</div>
{:else}
<p class="threats muted">Nothing hostile is one gesture away.</p>
{/if}
<div class="hands">
<HandPicker {duel} hand="left" />
<HandPicker {duel} hand="right" />
</div>
<div class="prefs">
<label class="pref">
<input type="checkbox" checked={duel.showPlans} onchange={(e) => duel.setShowPlans(e.currentTarget.checked)} />
Show spells under way beneath each hand
</label>
<label class="pref">
<input type="checkbox" checked={duel.showLessons} onchange={(e) => duel.setShowLessons(e.currentTarget.checked)} />
Explain rules as they come up
</label>
</div>
{#if duel.hasted}
<p class="haste-note">You are hastened: a second pair of gestures follows the first, and both take effect together.</p>
<div class="hands">
<HandPicker {duel} hand="left" set="haste" />
<HandPicker {duel} hand="right" set="haste" />
</div>
{/if}
<div class="orders">
{#if duel.stabbing}
<label>
<span>Stab</span>
<select bind:value={duel.stabTarget}>
<option value={FOE}>{duel.foe.name}</option>
{#each foeMonsters as m (m.id)}
<option value={m.id}>{m.name}</option>
{/each}
</select>
</label>
{/if}
{#each duel.yourMonsters as m (m.id)}
<label>
<span>{m.name} attacks</span>
<select bind:value={duel.monsterOrders[m.id]}>
<option value={FOE}>{duel.foe.name}</option>
{#each foeMonsters as fm (fm.id)}
<option value={fm.id}>{fm.name}</option>
{/each}
</select>
</label>
{/each}
{#if duel.bankedSpell}
<label>
<span>Release the banked {duel.bankedSpell.name.toLowerCase()}</span>
<select bind:value={duel.release}>
<option value="">not yet</option>
{#each duel.targetsFor(duel.bankedSpell) as t (t.id)}
<option value={t.id}>at {t.label}</option>
{/each}
</select>
</label>
{/if}
{#if duel.bankCandidates.length > 1}
<label>
<span>Bank</span>
<select bind:value={duel.bankPick}>
{#each duel.bankCandidates as p (castKey(p.set, p.hand))}
<option value={castKey(p.set, p.hand)}>{p.completion.spell.name} ({p.hand}{p.set === 'haste' ? ', second pair' : ''})</option>
{/each}
</select>
</label>
{/if}
{#if duel.permanentCandidates.length > 1}
<label>
<span>Make permanent</span>
<select bind:value={duel.permanentPick}>
{#each duel.permanentCandidates as p (castKey(p.set, p.hand))}
<option value={castKey(p.set, p.hand)}>{p.completion.spell.name} ({p.hand}{p.set === 'haste' ? ', second pair' : ''})</option>
{/each}
</select>
</label>
{/if}
{#if duel.youCharmedFoe}
<label>
<span>{duel.foe.name}'s charmed {duel.foe.constraints.charmed?.hand} hand makes</span>
<select bind:value={duel.charmGesture}>
{#each MAGIC_GESTURES as g (g)}
<option value={g}>{glyph(g)} {GESTURE_SHORT[g]}</option>
{/each}
</select>
</label>
{/if}
</div>
{#if duel.surrendering}
<p class="warning">Both palms at once is surrender. You will lose the duel.</p>
{/if}
{#if duel.conflict}
<p class="warning">Those two spells share a gesture. A gesture can finish only one spell; let one of them pass.</p>
{/if}
<button type="button" class="reveal" disabled={!duel.ready} onclick={() => duel.reveal()}>
{duel.surrendering ? 'Surrender' : duel.timeStopped ? 'Act in the stopped moment' : 'Reveal gestures'}
</button>
</section>
{/if}
</section>
<aside class="rail">
<WizardStatus wizard={duel.you} monsters={duel.yourMonsters} you />
<WizardStatus wizard={duel.foe} monsters={foeMonsters} />
<SpellSheet {duel} />
<Chronicle log={duel.state.log} />
</aside>
</main>
<MobileBar {duel} />
<footer class="colophon">
<p>After Richard Bartle's <em>Waving Hands</em> (1977), also known as Spellbinder and Spellcaster. <a href="/rules">Read the full rules</a>.</p>
</footer>
<style>
.masthead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1rem var(--gutter) 0.6rem;
max-width: 1280px;
margin: 0 auto;
}
h1 {
font-size: clamp(2rem, 4vw, 2.7rem);
font-weight: 300;
font-variation-settings: 'opsz' 144;
letter-spacing: -0.01em;
}
.standfirst {
max-width: 38em;
color: var(--bone-dim);
margin-top: 0.4rem;
}
.standfirst em {
color: var(--bone);
}
.actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.quiet {
background: none;
border: 1px solid var(--rule-strong);
border-radius: 4px;
padding: 0.4rem 0.9rem;
white-space: nowrap;
color: var(--bone);
text-decoration: none;
line-height: 1.55;
}
.quiet:hover {
border-color: var(--bone);
}
.confirm {
max-width: 1280px;
margin: 0 auto 0.75rem;
padding: 0.75rem var(--gutter);
}
.confirm p {
color: var(--blood);
margin-bottom: 0.5rem;
}
.choices {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.reveal.small {
margin: 0;
padding: 0.4rem 0.9rem;
font-size: 1rem;
}
details.next summary {
cursor: pointer;
list-style: none;
}
details.next summary::before {
content: '\25B8 ';
color: var(--bone-faint);
}
details.next[open] summary::before {
content: '\25BE ';
}
.board {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
gap: 0 clamp(1.5rem, 4vw, 3.5rem);
padding: 0 var(--gutter);
max-width: 1280px;
margin: 0 auto;
}
.play {
min-width: 0;
}
.rail {
min-width: 0;
}
.move {
padding: 0.6rem 0 1.5rem;
}
.move h2 {
font-size: 1.3rem;
margin-bottom: 0.3rem;
}
.threats {
font-size: 0.9rem;
margin-bottom: 0.7rem;
}
.threats ul {
list-style: none;
margin: 0.15rem 0 0.4rem;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.3rem 0.5rem;
}
.threat {
background: var(--slate);
border: 1px solid transparent;
border-radius: 3px;
padding: 0.15rem 0.5rem;
display: inline-flex;
gap: 0.5rem;
align-items: baseline;
color: var(--bone-dim);
}
.threat.soon {
color: var(--bone);
}
.threat:hover,
.threat.active {
border-color: var(--frost);
}
.threat.active {
background: rgba(140, 200, 224, 0.12);
}
.thand {
font-style: italic;
font-size: 0.8rem;
}
.tseq {
letter-spacing: 0.04em;
}
.tseq strong {
font-weight: 600;
color: var(--ember);
}
.prefs {
display: flex;
flex-wrap: wrap;
gap: 0.3rem 1.5rem;
margin-top: 0.7rem;
}
.pref {
display: flex;
gap: 0.4rem;
align-items: center;
font-size: 0.85rem;
color: var(--bone-dim);
}
.colophon a {
color: var(--frost);
}
.hands {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem 2rem;
}
.orders {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.5rem;
margin-top: 0.9rem;
}
.orders label {
display: inline-flex;
gap: 0.4rem;
align-items: baseline;
}
.orders label span {
color: var(--bone-dim);
font-style: italic;
}
.warning {
color: var(--blood);
margin-top: 0.8rem;
}
.haste-note {
color: var(--frost);
font-size: 0.95rem;
margin: 1rem 0 0.6rem;
}
.reveal {
margin-top: 0.8rem;
background: var(--bone);
color: var(--slate-deep);
border: 0;
border-radius: 4px;
padding: 0.6rem 1.4rem;
font-size: 1.05rem;
font-weight: 500;
}
.reveal:hover:not(:disabled) {
background: #fff;
}
.reveal:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.verdict {
padding: 1.5rem 0;
}
.verdict h2 {
font-size: 2rem;
font-style: italic;
color: var(--blood);
}
.verdict.won h2 {
color: var(--ember);
}
.verdict p {
margin: 0.3rem 0 0.8rem;
color: var(--bone-dim);
}
.colophon {
max-width: 1280px;
margin: 0 auto;
padding: 2rem var(--gutter) 3rem;
color: var(--bone-faint);
font-size: 0.85rem;
}
@media (max-width: 860px) {
.board {
grid-template-columns: minmax(0, 1fr);
}
.colophon {
padding-bottom: 6rem;
}
/* The sticky bar carries the reveal button on phones. */
.move > .reveal {
display: none;
}
.rail {
order: 2;
border-top: 1px solid var(--rule-strong);
margin-top: 1rem;
}
.hands {
grid-template-columns: 1fr;
}
.masthead {
flex-direction: column;
}
}
</style>
+285
View File
@@ -0,0 +1,285 @@
<script lang="ts">
import raw from '../../../docs/waving-hands-rules.txt?raw';
import { MONSTER_STATS, SPELLS, type SpellCategory } from '$lib/game/spells';
const groups: { category: SpellCategory; title: string; blurb: string }[] = [
{ category: 'protection', title: 'Protection', blurb: 'Usually cast on yourself. A shield is a single P, so a palm is never wasted.' },
{ category: 'summons', title: 'Summons', blurb: 'Creatures fight for whoever the spell is cast on, attack the same turn they appear, and keep attacking each turn until destroyed.' },
{ category: 'damage', title: 'Damage', blurb: 'A wizard can take 14 points; the fifteenth kills. Cures apply in the same turn as the damage.' },
{ category: 'enchantment', title: 'Enchantments', blurb: 'Most bind the subject on the following turn. Two of the mind-affecting kind landing at once cancel each other.' }
];
const HEADINGS = [
'Introduction',
'The Turn',
'Gestures',
'Winning',
'Spells',
'Protection spells',
'Summons spells',
'Damaging Spells',
'Enchantments',
'Non-spells',
'Reference Sheet',
'Sample Game',
'Annotation'
];
/** Split the transcription at its headings so each part can be linked. */
const original = (() => {
const lines = raw.split('\n');
const sections: { title: string; id: string; body: string }[] = [];
let current: { title: string; id: string; lines: string[] } | null = null;
for (const line of lines) {
const trimmed = line.trim();
if (HEADINGS.includes(trimmed)) {
if (current) sections.push({ title: current.title, id: current.id, body: current.lines.join('\n').trim() });
current = { title: trimmed, id: 'original-' + trimmed.toLowerCase().replace(/\s+/g, '-'), lines: [] };
continue;
}
if (/^\s*(Spellcaster|Rules for the Spellcaster Game|Back to Main List)\s*$/.test(line)) continue;
if (current) current.lines.push(line);
}
if (current) sections.push({ title: current.title, id: current.id, body: current.lines.join('\n').trim() });
return sections;
})();
const monsters = (['goblin', 'ogre', 'troll', 'giant', 'fire_elemental', 'ice_elemental'] as const).map((k) => MONSTER_STATS[k]);
</script>
<svelte:head>
<title>Waving Hands: the rules</title>
</svelte:head>
<main class="rules">
<header>
<a class="back" href="/">Back to the duel</a>
<h1>How to duel</h1>
<p class="lede">Waving Hands is Richard Bartle's 1977 game of two wizards trading spells made of hand gestures. This page is a working reference; the original transcription is at the end.</p>
<nav aria-label="Sections">
<a href="#turn">A turn</a>
<a href="#gestures">Gestures</a>
<a href="#overlap">Overlapping spells</a>
<a href="#targets">Targets and timing</a>
<a href="#monsters">Monsters</a>
<a href="#winning">Winning</a>
<a href="#spells">Every spell</a>
<a href="#original">Original text</a>
</nav>
</header>
<section id="turn">
<h2>A turn</h2>
<ol>
<li>Both wizards secretly choose a gesture for each hand. In this game you pick them, then choose which finished spells to cast and at whom.</li>
<li>Both reveal at once. Every spell, stab and monster attack of the turn takes effect together, so a shield you make this turn stops a missile made this turn.</li>
<li>Damage and healing are added up, monsters that lost all their hit points are removed, and enchantments cast this turn bind next turn.</li>
</ol>
</section>
<section id="gestures">
<h2>Gestures</h2>
<p>Six magical gestures: <strong>F</strong> wriggled fingers, <strong>P</strong> proffered palm, <strong>S</strong> snap, <strong>W</strong> wave, <strong>D</strong> pointed digit and <strong>C</strong> clap. A clap needs both hands; a clap with one hand is nothing. A hand may instead <strong>stab</strong> with the knife (one point of damage, one stab a turn) or do <strong>nothing</strong>. Neither belongs to any spell, so a hand that stabs or rests starts its next spell from scratch.</p>
<p>A spell is a run of gestures made on consecutive turns by one hand. In the notation, a bracketed lower-case letter such as <strong>(w</strong> means that gesture made with both hands at once.</p>
<p>Both palms at once is <strong>surrender</strong>. It is not a spell and it ends the duel.</p>
</section>
<section id="overlap">
<h2>Overlapping spells</h2>
<p>A gesture can belong to more than one spell as long as each spell's run is unbroken. F-F-F is Paralysis; follow it with S-S-D-D and the last five gestures, F-S-S-D-D, are a Fireball. Another F after F-F-F is a second Paralysis. Each gesture can finish only one spell, so if two spells would end on the same gesture you choose one. Any finished spell may be let go harmlessly, except the storms, which rage wherever they are released.</p>
</section>
<section id="targets">
<h2>Targets and timing</h2>
<p>Every spell has a subject. Protection and summons are usually cast on yourself; attacks and enchantments on your opponent; but any spell may be aimed at any wizard or monster, and a spell aimed at something that no longer exists is lost.</p>
<p>Because everything is simultaneous, a monster killed the turn it appears still attacks that turn, a Resist heat cast in the same turn as a Fireball saves you, and a Remove enchantment cast in the same turn as a Fireball leaves you burned. <strong>Counter-spell</strong> stops every other spell cast at its subject that turn, except Finger of death and Dispel magic. <strong>Dispel magic</strong> stops every spell on the field, ends every enchantment and unmakes every monster after it attacks.</p>
</section>
<section id="monsters">
<h2>Monsters</h2>
<p>A summoned creature obeys whoever it was summoned for, attacks any wizard or monster its master names, and is stopped by a shield on its target. Elementals attack everyone not resistant to their element, are destroyed by the opposite element, and merge if two of a kind meet.</p>
<table>
<thead><tr><th>Creature</th><th>Hit points</th><th>Damage a turn</th></tr></thead>
<tbody>
{#each monsters as m (m.name)}
<tr><td>{m.name}</td><td>{m.hp}</td><td>{m.attack}</td></tr>
{/each}
</tbody>
</table>
</section>
<section id="winning">
<h2>Winning</h2>
<p>Each wizard has fifteen hit points and dies on losing them all. If both die in the same turn the duel is a draw. Surrender loses, unless the surrendering wizard's last gestures also killed the opponent.</p>
</section>
<section id="spells">
<h2>Every spell</h2>
{#each groups as g (g.category)}
<h3 id={'spells-' + g.category}>{g.title}</h3>
<p class="blurb">{g.blurb}</p>
<dl>
{#each SPELLS.filter((s) => s.category === g.category) as spell (spell.id)}
<div>
<dt><span class="name">{spell.name}</span> <span class="seq">{spell.sequences.join(' or ')}</span></dt>
<dd>{spell.summary}</dd>
</div>
{/each}
</dl>
{/each}
</section>
<section id="original">
<h2>The original text</h2>
<p class="blurb">Bartle's rules as printed in the <em>Duel Purpose</em> fanzine, transcribed by Andrew Buchanan. Kept as the source for anything this page simplifies.</p>
<nav aria-label="Original sections" class="subnav">
{#each original as sec (sec.id)}
<a href={'#' + sec.id}>{sec.title}</a>
{/each}
</nav>
{#each original as sec (sec.id)}
<details id={sec.id}>
<summary>{sec.title}</summary>
<pre>{sec.body}</pre>
</details>
{/each}
</section>
</main>
<style>
.rules {
max-width: 44rem;
margin: 0 auto;
padding: 1.5rem var(--gutter) 4rem;
}
a {
color: var(--frost);
}
.back {
font-size: 0.95rem;
}
h1 {
font-size: 2.4rem;
font-weight: 300;
font-variation-settings: 'opsz' 144;
margin: 0.5rem 0 0.3rem;
}
.lede {
color: var(--bone-dim);
max-width: 38em;
}
nav {
display: flex;
flex-wrap: wrap;
gap: 0.3rem 1rem;
margin: 1rem 0 0.5rem;
font-size: 0.95rem;
}
section {
padding: 1.25rem 0 0.5rem;
border-top: 1px solid var(--rule);
margin-top: 1rem;
}
h2 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
h3 {
font-size: 1.15rem;
font-style: italic;
margin-top: 1.25rem;
}
p + p,
ol {
margin-top: 0.6rem;
}
ol {
padding-left: 1.4rem;
}
li + li {
margin-top: 0.3rem;
}
.blurb {
color: var(--bone-dim);
font-size: 0.95rem;
margin: 0.2rem 0 0.6rem;
}
table {
border-collapse: collapse;
margin-top: 0.75rem;
font-size: 0.95rem;
}
th,
td {
text-align: left;
padding: 0.25rem 1.5rem 0.25rem 0;
border-bottom: 1px solid var(--rule);
}
th {
font-weight: 500;
color: var(--bone-dim);
}
dl {
margin: 0;
}
dl > div {
padding: 0.45rem 0;
border-bottom: 1px solid var(--rule);
}
dt .name {
font-weight: 500;
}
dt .seq {
margin-left: 0.6rem;
letter-spacing: 0.05em;
color: var(--ember);
}
dd {
margin: 0.15rem 0 0;
color: var(--bone-dim);
font-size: 0.95rem;
}
.subnav {
font-size: 0.9rem;
}
details {
border-bottom: 1px solid var(--rule);
padding: 0.4rem 0;
}
summary {
cursor: pointer;
font-style: italic;
}
pre {
font-family: var(--font);
font-size: 0.95rem;
line-height: 1.55;
white-space: pre-wrap;
margin: 0.5rem 0 0.5rem;
color: var(--bone-dim);
}
</style>