607 lines
18 KiB
HTML
607 lines
18 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
<title>Decode the Synchrotron Signal</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
--panel: rgba(10, 16, 25, 0.92);
|
|
--line: rgba(255, 255, 255, 0.16);
|
|
--text: #f8f3ea;
|
|
--muted: #aeb8c8;
|
|
--cyan: #65d8e8;
|
|
--gold: #f2c96b;
|
|
--green: #88d76f;
|
|
--red: #ef735f;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html,
|
|
body {
|
|
min-height: 100%;
|
|
margin: 0;
|
|
}
|
|
|
|
body {
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
background:
|
|
radial-gradient(circle at 16% 12%, rgba(101, 216, 232, 0.22), transparent 28%),
|
|
radial-gradient(circle at 86% 78%, rgba(242, 201, 107, 0.13), transparent 32%),
|
|
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
|
color: var(--text);
|
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
}
|
|
|
|
.shell {
|
|
min-height: 100vh;
|
|
display: grid;
|
|
grid-template-rows: auto 1fr auto;
|
|
gap: 14px;
|
|
padding: 16px;
|
|
}
|
|
|
|
header,
|
|
footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.brand {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.brand img {
|
|
width: clamp(48px, 8vw, 72px);
|
|
opacity: 0.58;
|
|
}
|
|
|
|
.brand small {
|
|
color: var(--cyan);
|
|
font-size: 0.78rem;
|
|
font-weight: 900;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0;
|
|
font-size: clamp(1.45rem, 3.5vw, 2.55rem);
|
|
line-height: 1;
|
|
}
|
|
|
|
.stats {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
.stat,
|
|
.panel,
|
|
button {
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
background: var(--panel);
|
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
|
|
.stat {
|
|
min-width: 104px;
|
|
padding: 8px 10px;
|
|
}
|
|
|
|
.stat span {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 0.7rem;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.stat strong {
|
|
display: block;
|
|
margin-top: 2px;
|
|
font-size: 1.12rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
main {
|
|
position: relative;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(320px, 1.15fr) minmax(280px, 0.85fr);
|
|
gap: 14px;
|
|
}
|
|
|
|
main::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 10% 14%;
|
|
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
|
opacity: 0.08;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.panel {
|
|
position: relative;
|
|
z-index: 1;
|
|
padding: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.category {
|
|
display: inline-flex;
|
|
width: fit-content;
|
|
margin-bottom: 14px;
|
|
padding: 7px 10px;
|
|
border: 1px solid rgba(101, 216, 232, 0.32);
|
|
border-radius: 8px;
|
|
color: var(--cyan);
|
|
font-size: 0.8rem;
|
|
font-weight: 900;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
background: rgba(101, 216, 232, 0.09);
|
|
}
|
|
|
|
.message {
|
|
min-height: 42px;
|
|
margin: 0 0 12px;
|
|
color: var(--gold);
|
|
font-size: clamp(1rem, 2vw, 1.25rem);
|
|
font-weight: 850;
|
|
line-height: 1.32;
|
|
}
|
|
|
|
.phrase {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-content: flex-start;
|
|
gap: 12px 18px;
|
|
min-height: 330px;
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.word {
|
|
display: flex;
|
|
gap: 5px;
|
|
}
|
|
|
|
.tile {
|
|
width: clamp(32px, 4.7vw, 52px);
|
|
display: grid;
|
|
gap: 4px;
|
|
place-items: center;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.cipher {
|
|
width: 100%;
|
|
color: var(--muted);
|
|
font-size: 0.72rem;
|
|
font-weight: 900;
|
|
text-align: center;
|
|
}
|
|
|
|
.plain {
|
|
width: 100%;
|
|
aspect-ratio: 1;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-bottom: 3px solid rgba(101, 216, 232, 0.65);
|
|
border-radius: 7px;
|
|
background: rgba(255, 255, 255, 0.07);
|
|
font-size: clamp(1.05rem, 2.8vw, 1.65rem);
|
|
font-weight: 950;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.tile.active .plain {
|
|
border-color: rgba(242, 201, 107, 0.95);
|
|
box-shadow: 0 0 0 4px rgba(242, 201, 107, 0.16);
|
|
}
|
|
|
|
.tile.correct .plain {
|
|
color: #061106;
|
|
background: var(--green);
|
|
border-color: var(--green);
|
|
}
|
|
|
|
.side {
|
|
display: grid;
|
|
grid-template-rows: auto auto auto 1fr;
|
|
gap: 10px;
|
|
}
|
|
|
|
.hint h2 {
|
|
margin: 0 0 10px;
|
|
font-size: clamp(1.4rem, 3vw, 2.1rem);
|
|
}
|
|
|
|
.hint p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
font-size: clamp(1rem, 2vw, 1.15rem);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 6px;
|
|
}
|
|
|
|
.difficulty {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(72px, 100px));
|
|
gap: 5px;
|
|
justify-content: start;
|
|
}
|
|
|
|
.keyboard {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
gap: 7px;
|
|
align-content: end;
|
|
}
|
|
|
|
button {
|
|
min-height: 40px;
|
|
padding: 0 8px;
|
|
border-color: rgba(255, 255, 255, 0.18);
|
|
color: var(--text);
|
|
font: inherit;
|
|
font-weight: 900;
|
|
cursor: pointer;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
button.primary {
|
|
color: #071014;
|
|
background: linear-gradient(135deg, var(--gold), #ffdc83);
|
|
border-color: rgba(255, 220, 131, 0.9);
|
|
}
|
|
|
|
button.active-level {
|
|
color: #071014;
|
|
background: var(--green);
|
|
border-color: rgba(136, 215, 111, 0.85);
|
|
}
|
|
|
|
button.letter {
|
|
min-height: 48px;
|
|
color: #071014;
|
|
background: var(--cyan);
|
|
border-color: rgba(101, 216, 232, 0.8);
|
|
font-size: clamp(1rem, 1.8vw, 1.24rem);
|
|
}
|
|
|
|
.controls button {
|
|
min-height: 38px;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.difficulty button {
|
|
min-height: 28px;
|
|
padding: 0 6px;
|
|
font-size: 0.76rem;
|
|
}
|
|
|
|
button:disabled {
|
|
opacity: 0.38;
|
|
cursor: default;
|
|
}
|
|
|
|
.ghost {
|
|
color: var(--muted);
|
|
background: rgba(255, 255, 255, 0.07);
|
|
}
|
|
|
|
@media (max-width: 840px) {
|
|
header,
|
|
footer {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.stats {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
main {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.phrase {
|
|
min-height: 250px;
|
|
}
|
|
|
|
.keyboard {
|
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<header>
|
|
<div class="brand">
|
|
<img src="assets/txproducts-logo-watermark.png" alt="">
|
|
<div>
|
|
<small>txproducts.de signal puzzle</small>
|
|
<h1>Decode the Synchrotron Signal</h1>
|
|
</div>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
|
<div class="stat"><span>Signal</span><strong id="signal">1 / 8</strong></div>
|
|
<div class="stat"><span>Errors</span><strong id="errors">0</strong></div>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="panel">
|
|
<div class="category" id="category">Product</div>
|
|
<p class="message" id="message">Tap an empty tile, then choose the decoded letter.</p>
|
|
<div class="phrase" id="phrase"></div>
|
|
</section>
|
|
|
|
<aside class="side">
|
|
<section class="panel hint">
|
|
<h2>Are you ready?</h2>
|
|
<p>Decode the hidden phrase. The small top letters are the encrypted signal; your job is to recover the real synchrotron message below.</p>
|
|
</section>
|
|
<section class="difficulty" aria-label="Difficulty">
|
|
<button class="active-level" data-level="Easy" type="button">Easy</button>
|
|
<button data-level="Medium" type="button">Medium</button>
|
|
<button data-level="Hard" type="button">Hard</button>
|
|
</section>
|
|
<section class="controls">
|
|
<button class="primary" id="hintButton">Hint</button>
|
|
<button id="checkButton">Check</button>
|
|
<button id="nextButton">Next</button>
|
|
</section>
|
|
<section class="keyboard" id="keyboard"></section>
|
|
</aside>
|
|
</main>
|
|
|
|
<footer>
|
|
<button class="ghost" id="resetButton">Reset</button>
|
|
<button class="ghost" onclick="window.location.href='index.html'">Back to menu</button>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
|
const puzzleSets = {
|
|
Easy: [
|
|
{ category: "Easy Product", phrase: "WAVEGATE", note: "WaveGate is the pulse timing product visitors should remember." },
|
|
{ category: "Easy Product", phrase: "PICOSWITCH", note: "PicoSwitch is the fast x-ray pulse shortening product." },
|
|
{ category: "Easy Data", phrase: "TXDATA", note: "TXData connects measurements with usable insight." },
|
|
{ category: "Easy Service", phrase: "TXSERVICES", note: "TXServices supports teams around experiments and beamtime." },
|
|
{ category: "Easy Physics", phrase: "XRAY PULSE", note: "X-ray pulses are central to these timing games." },
|
|
{ category: "Easy Business", phrase: "BETTER DATA", note: "Better data helps create better decisions." }
|
|
],
|
|
Medium: [
|
|
{ category: "Product", phrase: "WAVEGATE TAILORS PULSE STRUCTURE", note: "WaveGate shapes synchrotron pulse timing across nanoseconds to seconds." },
|
|
{ category: "Product", phrase: "PICOSWITCH CUTS X RAY PULSES", note: "PicoSwitch uses photo-acoustic Bragg switching for short x-ray pulses." },
|
|
{ category: "Physics", phrase: "BRAGG DIFFRACTION SELECTS THE BEAM", note: "Bragg diffraction is the physics clue behind precise switching." },
|
|
{ category: "Beamline", phrase: "SYNCHROTRON TIMING MATTERS", note: "Timing can decide whether a measurement is clean or noisy." },
|
|
{ category: "Data", phrase: "TXDATA TURNS MEASUREMENTS INTO INSIGHT", note: "TXData connects experimental output with decisions people can use." },
|
|
{ category: "Service", phrase: "TXSERVICES SUPPORTS BETTER BEAMTIME", note: "Good support helps teams get more value from scarce experiment time." },
|
|
{ category: "Business", phrase: "BETTER DATA BETTER DECISIONS", note: "The booth story links products, data, and service to business value." },
|
|
{ category: "Experiment", phrase: "COHERENT LATTICE DYNAMICS", note: "Short pulses can reveal ultrafast motion inside materials." }
|
|
],
|
|
Hard: [
|
|
{ category: "Hard Physics", phrase: "PHOTO ACOUSTIC BRAGG SWITCHING", note: "PicoSwitch uses photo-acoustic Bragg switching to shorten x-ray pulses." },
|
|
{ category: "Hard Timing", phrase: "TEMPORAL SYNCHROTRON PULSE STRUCTURE", note: "WaveGate tailors synchrotron pulse structure across broad timescales." },
|
|
{ category: "Hard Experiment", phrase: "COHERENT LATTICE DYNAMICS IN THIN FILMS", note: "Short x-ray pulses can reveal coherent lattice motion in materials." },
|
|
{ category: "Hard Business", phrase: "MEASUREMENTS BECOME ACTIONABLE INSIGHT", note: "TXData and TXServices help turn complex experimental output into decisions." },
|
|
{ category: "Hard Beamline", phrase: "DIFFRACTION EFFICIENCY LIMITS REPETITION RATE", note: "Technical constraints shape how a device performs in real experiments." },
|
|
{ category: "Hard Product", phrase: "NANOSECONDS TO SECONDS PULSE TAILORING", note: "That broad timing span is the memorable WaveGate story." }
|
|
]
|
|
};
|
|
let puzzles = puzzleSets.Easy;
|
|
const shifts = [7, 11, 5, 19, 9, 15, 21, 3];
|
|
const state = {
|
|
level: "Easy",
|
|
index: 0,
|
|
score: 0,
|
|
errors: 0,
|
|
selected: 0,
|
|
guesses: []
|
|
};
|
|
|
|
const phraseEl = document.getElementById("phrase");
|
|
const keyboardEl = document.getElementById("keyboard");
|
|
const messageEl = document.getElementById("message");
|
|
const scoreEl = document.getElementById("score");
|
|
const signalEl = document.getElementById("signal");
|
|
const errorsEl = document.getElementById("errors");
|
|
const categoryEl = document.getElementById("category");
|
|
const levelButtons = document.querySelectorAll("[data-level]");
|
|
|
|
function encodeLetter(letter) {
|
|
if (!/[A-Z]/.test(letter)) return letter;
|
|
const shift = shifts[state.index % shifts.length];
|
|
const next = (alphabet.indexOf(letter) + shift) % 26;
|
|
return alphabet[next];
|
|
}
|
|
|
|
function getLetters() {
|
|
return puzzles[state.index].phrase.split("").map((char, index) => ({
|
|
char,
|
|
cipher: encodeLetter(char),
|
|
index,
|
|
isLetter: /[A-Z]/.test(char)
|
|
}));
|
|
}
|
|
|
|
function startPuzzle() {
|
|
const puzzle = puzzles[state.index];
|
|
state.guesses = Array(puzzle.phrase.length).fill("");
|
|
state.selected = getLetters().find((item) => item.isLetter)?.index || 0;
|
|
categoryEl.textContent = `${state.level} / ${puzzle.category}`;
|
|
messageEl.textContent = "Tap an empty tile, then choose the decoded letter.";
|
|
render();
|
|
}
|
|
|
|
function render() {
|
|
const puzzle = puzzles[state.index];
|
|
scoreEl.textContent = state.score;
|
|
signalEl.textContent = `${state.index + 1} / ${puzzles.length}`;
|
|
errorsEl.textContent = state.errors;
|
|
phraseEl.innerHTML = "";
|
|
const words = puzzle.phrase.split(" ");
|
|
let offset = 0;
|
|
words.forEach((word) => {
|
|
const wordEl = document.createElement("div");
|
|
wordEl.className = "word";
|
|
for (const char of word) {
|
|
const index = offset;
|
|
const tile = document.createElement("button");
|
|
tile.className = "tile";
|
|
tile.type = "button";
|
|
if (index === state.selected) tile.classList.add("active");
|
|
if (state.guesses[index] === puzzle.phrase[index]) tile.classList.add("correct");
|
|
tile.innerHTML = `<span class="cipher">${encodeLetter(char)}</span><span class="plain">${state.guesses[index] || ""}</span>`;
|
|
tile.addEventListener("click", () => {
|
|
state.selected = index;
|
|
render();
|
|
});
|
|
wordEl.appendChild(tile);
|
|
offset += 1;
|
|
}
|
|
phraseEl.appendChild(wordEl);
|
|
offset += 1;
|
|
});
|
|
}
|
|
|
|
function renderKeyboard() {
|
|
keyboardEl.innerHTML = "";
|
|
alphabet.forEach((letter) => {
|
|
const button = document.createElement("button");
|
|
button.className = "letter";
|
|
button.textContent = letter;
|
|
button.addEventListener("click", () => guess(letter));
|
|
keyboardEl.appendChild(button);
|
|
});
|
|
}
|
|
|
|
function nextEmpty(fromIndex = 0) {
|
|
const puzzle = puzzles[state.index];
|
|
for (let i = fromIndex; i < puzzle.phrase.length; i += 1) {
|
|
if (/[A-Z]/.test(puzzle.phrase[i]) && !state.guesses[i]) return i;
|
|
}
|
|
for (let i = 0; i < puzzle.phrase.length; i += 1) {
|
|
if (/[A-Z]/.test(puzzle.phrase[i]) && !state.guesses[i]) return i;
|
|
}
|
|
return state.selected;
|
|
}
|
|
|
|
function guess(letter) {
|
|
const puzzle = puzzles[state.index];
|
|
if (!/[A-Z]/.test(puzzle.phrase[state.selected])) {
|
|
state.selected = nextEmpty();
|
|
}
|
|
state.guesses[state.selected] = letter;
|
|
if (letter === puzzle.phrase[state.selected]) {
|
|
state.score += 25;
|
|
messageEl.textContent = "Good decode. Keep going.";
|
|
} else {
|
|
state.errors += 1;
|
|
messageEl.textContent = "That signal is off. Try another tile.";
|
|
}
|
|
state.selected = nextEmpty(state.selected + 1);
|
|
render();
|
|
if (isSolved()) completePuzzle();
|
|
}
|
|
|
|
function isSolved() {
|
|
const puzzle = puzzles[state.index];
|
|
return puzzle.phrase.split("").every((char, index) => !/[A-Z]/.test(char) || state.guesses[index] === char);
|
|
}
|
|
|
|
function completePuzzle() {
|
|
const puzzle = puzzles[state.index];
|
|
state.score += 150;
|
|
messageEl.textContent = `Signal decoded: ${puzzle.note}`;
|
|
render();
|
|
}
|
|
|
|
function revealHint() {
|
|
const puzzle = puzzles[state.index];
|
|
const missing = puzzle.phrase.split("").findIndex((char, index) => /[A-Z]/.test(char) && state.guesses[index] !== char);
|
|
if (missing >= 0) {
|
|
state.guesses[missing] = puzzle.phrase[missing];
|
|
state.selected = nextEmpty(missing + 1);
|
|
messageEl.textContent = "Hint revealed one correct signal letter.";
|
|
render();
|
|
if (isSolved()) completePuzzle();
|
|
}
|
|
}
|
|
|
|
function checkPuzzle() {
|
|
if (isSolved()) {
|
|
completePuzzle();
|
|
return;
|
|
}
|
|
const puzzle = puzzles[state.index];
|
|
const wrong = puzzle.phrase.split("").filter((char, index) => /[A-Z]/.test(char) && state.guesses[index] && state.guesses[index] !== char).length;
|
|
messageEl.textContent = wrong ? `${wrong} decoded letters need another look.` : "No wrong letters yet. Keep decoding.";
|
|
}
|
|
|
|
function nextPuzzle() {
|
|
state.index = (state.index + 1) % puzzles.length;
|
|
startPuzzle();
|
|
}
|
|
|
|
function setLevel(level) {
|
|
state.level = level;
|
|
puzzles = puzzleSets[level];
|
|
state.index = 0;
|
|
state.score = 0;
|
|
state.errors = 0;
|
|
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
|
|
startPuzzle();
|
|
}
|
|
|
|
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
|
|
document.getElementById("hintButton").addEventListener("click", revealHint);
|
|
document.getElementById("checkButton").addEventListener("click", checkPuzzle);
|
|
document.getElementById("nextButton").addEventListener("click", nextPuzzle);
|
|
document.getElementById("resetButton").addEventListener("click", () => {
|
|
state.index = 0;
|
|
state.score = 0;
|
|
state.errors = 0;
|
|
startPuzzle();
|
|
});
|
|
window.addEventListener("keydown", (event) => {
|
|
const letter = event.key.toUpperCase();
|
|
if (alphabet.includes(letter)) guess(letter);
|
|
if (event.key === "Enter") checkPuzzle();
|
|
if (event.key.toLowerCase() === "r") {
|
|
state.score = 0;
|
|
state.errors = 0;
|
|
startPuzzle();
|
|
}
|
|
});
|
|
|
|
renderKeyboard();
|
|
startPuzzle();
|
|
</script>
|
|
</body>
|
|
</html>
|