1. Quick start
Three steps and the page is live with a working form.
- Open
fillup.phpin a plain text editor. Notepad is fine, Word is not. - Find
$TOnear the top and put your own email address there. - Upload the whole folder to your web host, keeping the folder structure.
Then send yourself one test brief from the page and check three things.
- The email arrives, and is not in your spam folder.
- A file appeared at
storage/briefs-2026-01.csvwith your test in it. - Typing that CSV address into a browser gives 403 or 404, never a download.
Do the third one before you take real enquiries. That folder holds your clients' names, event dates and budgets. Section 7 explains what to do if the file downloads.
What you need
A host that runs PHP 7.4 or newer, which is nearly all of them. Nothing to install, no Composer, no libraries, no account anywhere. The page itself is plain HTML and CSS and works on any host at all; only the form needs PHP.
2. What is in the folder
fillup.php your settings and wording. The only file you have to edit.
send.php receives the brief, checks it, saves it, emails it
css/style.css all styling, with every colour set at the top
js/main.js one file, no libraries
images/ every picture on the page, JPEG and WebP
storage/ where briefs are saved. Read the note inside.
docs/ this documentation
README.txt the short version of this file
The two files you will touch most are index.html for wording on the page and fillup.php for wording in the emails. You should not need to open send.php at all.
3. The sections
Every block in index.html starts with a comment like <!-- ===== 4. What we run ===== -->, so searching for the number takes you straight there.
| # | Section | What it is for |
|---|---|---|
| 1 | Header | Sticky, and transparent to begin with so the dark hero shows through. It turns solid once the page has moved. |
| 2 | Hero | Dark. Large type on the left, two overlapping photographs on the right. |
| 3 | Ribbon | A line of event types sliding slowly sideways. Pure CSS. |
| 4 | What we run | Five tabs. Radio buttons and CSS, no JavaScript. |
| 5 | What we handle | A ten item checklist beside a photograph and a small card. |
| 6 | Parallax band | A photograph that scrolls slower than the page, with a statement over it. |
| 7 | How we work | Five stages either side of a centre line. |
| 8 | Recent work | Six photographs with captions on them. Any of them opens in a popup. |
| 9 | The brief form | Nine fields on a dark band, in one centred column. |
| 10 | Questions | Six questions and answers, two across, nothing hidden. |
| 11 | Closing | A short dark band with one button. |
| 12 | Footer | Copyright line and the credit. |
Removing a section
Delete the whole block, from its comment down to the closing </section>. Nothing else breaks, except that any link in the header pointing at it should go too. The one section you cannot delete is number 9, because every button on the page scrolls to it.
4. Colours and fonts
Every colour is a variable at the top of css/style.css. Change one there and it changes everywhere.
:root {
--ink: #17131F; /* headings, body text, primary button */
--ink-soft: #5C5468; /* second-rank text */
--gold: #A0731C; /* the accent, on pale backgrounds */
--gold-br: #E0B44A; /* the accent, on dark backgrounds */
--cream: #FAF6EF; /* the pale section background */
--line: #E7DFD2; /* hairlines */
--dark: #17131F; /* the dark sections */
--dark-2: #221C2E; /* panels sitting on a dark section */
}
Two golds, on purpose. --gold is dark enough to read as text on cream. --gold-br is bright enough to read on the near-black sections. Swapping in one colour for both is the quickest way to make this template look wrong, because one of the two places will lose its contrast.
Fonts
Headings and anything structural use the system sans-serif face. Body copy is a serif. That split is most of the page's character, so if you change one, change the other to match. Both are fonts the reader already has, so there is nothing to download and nothing slowing the page down.
To use a web font instead, add its <link> in the head of index.html, then change the font line in the body rule for body copy, and the font-family in the h1, h2, h3, .sans rule for headings.
Corners
--r and --r-lg are small on purpose: this template is nearly square cornered, which is what makes it read as formal next to a rounder design. Raise them both to about 14px and 22px for a softer look.
5. The tabs, and adding one
Section 4 of the page has five tabs. There is no JavaScript behind them at all. They are radio buttons, hidden from sight but not from the keyboard, and CSS decides which panel to show based on which one is checked.
Three things follow from that. They work with scripting switched off. The arrow keys move between them, because that is what a group of radio buttons does by itself. And there is no flash of every panel at once while a script loads, because no script is loading.
The shape of it
<div class="tabs">
<input class="tab-radio" type="radio" name="what" id="tab1" checked>
<input class="tab-radio" type="radio" name="what" id="tab2">
... one input per tab ...
<div class="tab-bar">
<label for="tab1">Conferences and expos</label>
<label for="tab2">Galas and award nights</label>
... one label per tab, in the same order ...
</div>
<div class="tab-panels">
<article class="tab-panel"> ... </article>
<article class="tab-panel"> ... </article>
... one panel per tab, in the same order ...
</div>
</div>
The order matters in all three lists. Input one goes with label one and panel one.
Adding a sixth
- Add
<input class="tab-radio" type="radio" name="what" id="tab6">after the fifth input. - Add
<label for="tab6">Your title</label>at the end of the tab bar. - Copy a whole
<article class="tab-panel">to the end of the panels and change its words and picture. - In
css/style.css, find the two rules that list the tab numbers and add a line to each:
#tab6:checked ~ .tab-bar label:nth-of-type(6) { ... }
#tab6:checked ~ .tab-panels .tab-panel:nth-of-type(6) { display: grid; }
Step four is the one people miss. A sixth tab with no matching CSS never appears when you click it, and nothing tells you why. Both rules are marked with a comment in the stylesheet saying to copy a pair and bump both numbers.
Removing a tab is the same in reverse: delete the input, the label, the panel and the two lines. If you remove the first one, move checked onto whichever input is now first, or the section opens with nothing showing.
6. The brief form
One form, nine fields. It asks for enough to write a sensible first reply and no more.
| Field | Name in the HTML | Required |
|---|---|---|
| Your name | name | no |
email | yes | |
| Phone | phone | no |
| Company or organisation | company | no |
| Kind of event | event | no |
| Date | eventdate | no |
| How many people | guests | no |
| Venue | venue | no |
| Budget range | budget | no |
| The brief | message | no |
Why the email is the required one. A proposal for an event gets emailed, not read out over the phone. If you would rather chase people by telephone, open send.php, find the two validation blocks near the comment that says so, and swap which one is allowed to be empty.
The date box
It is a real <input type="date">, so a modern browser shows its own date picker and sends the date as 2026-11-14. An older browser turns it into a plain text box instead, which is why send.php accepts anything sensible there rather than insisting on a format.
The wording in the emails
All of it is in fillup.php, in seven numbered parts. Words in curly brackets are swapped for the real details when the message is sent:
{name} {email} {phone} {company} {event} {eventdate} {guests} {venue} {budget} {message} {site} {date}
{eventdate} and {date} are not the same thing. {eventdate} is the day of their event. {date} is the moment the brief was sent. Mixing the two up is easy and reads badly in a confirmation email.
Spell every token exactly, in lower case. A token spelled wrong prints as written rather than vanishing, which is how you spot it.
Spam
Three things run on every submission.
- A honeypot. A field named
website, positioned off the side of the screen. Nobody using the site ever sees it. Anything that fills it in is a robot. - A speed check. The page stamps the time it loaded into the
tsfield. Anything sent within two and a half seconds of that was not typed by a person. - A rate limit. Five submissions an hour from one address, set by
$MAX_PER_HOUR. Addresses are stored hashed, sostorage/rate.jsonholds no personal data.
A caught submission is shown the ordinary thank you rather than an error. Telling a robot it was caught only teaches whoever wrote it what to change. The details go to storage/trapped-*.csv, so a real person caught by mistake is never lost. Look in that file now and then.
Do not delete the hidden fields. The form carries website and ts. Remove them and your spam rises sharply within days.
7. Where briefs are saved
Every brief is written to a CSV file before any email is attempted. An email can fail for a dozen reasons outside your control; a written file cannot. The worst an email problem can cost you is a notification, never the enquiry.
storage/trapped-2026-01.csv what the spam traps stopped
storage/mail-failures.log written only when an email did not go out
storage/rate.json counts recent submissions, addresses hashed
Open the CSV in Excel or Google Sheets. It is written with a byte order mark so accented characters come out right.
Keeping the folder private
There is an .htaccess file inside storage/ that blocks web access, written in both the Apache 2.2 and the Apache 2.4 way so it works on either. That covers most shared hosting.
If your host runs nginx, .htaccess is ignored. Either move the storage folder above your web root and point $LOG_DIR in fillup.php at the new place, which is the better answer, or add a deny rule for /storage/ to your nginx config.
Either way, after your site is live, type the address of one CSV file into a browser. You want 403 or 404. If it downloads, your clients' budgets are public.
8. If email does not arrive
The brief is already saved, so nothing is lost. What has failed is only the notification.
PHP's built-in mail function is unreliable on some shared hosting, and mail sent that way often lands in spam because nothing proves it came from you. The fix is to send through your own mailbox instead. Fill in the $SMTP block at the bottom of fillup.php and change 'enabled' to true.
| Line | What to put |
|---|---|
host | Your outgoing mail server. Whoever gives you email tells you this. Usually smtp.yourdomain.com, smtp.gmail.com or mail.yourhost.com. |
port | 587 in most cases. 465 only if your provider says so. |
secure | tls goes with 587, ssl goes with 465. They come as a pair. |
user | Your full email address, including the @ part. Not just the bit before it. |
pass | That mailbox's password. |
Gmail, Outlook and Yahoo will refuse your normal password. They make you create a separate "app password" in your account security settings, and that is what goes in pass. If signing in keeps failing, this is almost always the reason.
Also set $FROM near the top of fillup.php to the same address as user. Many providers refuse a message that claims to come from somewhere else.
The password sits in the file as ordinary text. Use a mailbox created for the website rather than your personal one, and never put a copy of fillup.php anywhere public.
Would you rather use PHPMailer?
It drops in without changing anything else. Replace the body of the send_mail() function in send.php with your PHPMailer calls; every other part of the script carries on as it is. The built-in sender is here so that the template works with nothing installed, not because it is better.
9. Images
Every picture is in images/, twice: a .jpg that works everywhere and a .webp that is roughly half the size. The browser picks the smaller one it understands.
| File | Size | Where it appears |
|---|---|---|
hero-a | 640 x 800 | The large hero photograph, portrait |
hero-b | 520 x 390 | The smaller one overlapping it |
tab-expo, tab-gala, tab-festival, tab-summit, tab-workshop | 900 x 620 | One per tab, section 4 |
handle | 800 x 640 | Beside the checklist, section 5 |
band | 1600 x 900 | The parallax band, section 6 |
g1 to g6 | 1200 x 900 | Recent work, and the popup |
top | 144 x 144 | The back to top button |
Swapping a picture
- Crop your photograph to the size in the table above.
- Save it over the old file, keeping the same name.
- If you cannot make a WebP, delete the matching
.webpfile and remove its<source>line. The JPEG then does the work on its own.
Keep the width and height attributes matching the real file. They are on every <img> tag. They tell the browser how much room to leave before the picture arrives, and getting them wrong makes the page jump about while it loads.
The photographs are placeholders. They are not covered by the licence on this template. Replace them with your own, or with pictures you have the right to use, before you publish.
The logo
It is drawn in the HTML, not a picture file, so it is sharp everywhere and takes its colour from --gold-br. Replace the <svg> inside <span class="logo-mark"> with your own, or swap the whole span for an <img> of your logo.
10. Recent work and the popup
Six photographs, three across. Every tile is a real <button>, which means it works from the keyboard as well as the mouse.
<button class="work-item" type="button" data-cap="Awards night, 320 guests">
<picture>
<source type="image/webp" srcset="images/g1.webp">
<img src="images/g1.jpg" alt="..." width="1200" height="900" loading="lazy" decoding="async">
</picture>
<span class="work-cap">Awards night, 320 guests</span>
</button>
The caption appears twice on purpose: once inside data-cap, which is what the popup prints, and once as the <span> printed on the photograph itself. Change both, or they will disagree.
That caption sits on the picture rather than appearing on hover, because there is no hover on a phone and a caption nobody can reach is not a caption.
Add or remove tiles freely; the popup counts whatever it finds. Inside it, the arrows move between photographs, the arrow keys do the same, Escape closes it, and a swipe works on a phone. While it is open the keyboard stays inside it, and when it closes the focus returns to the tile you came from.
Why the popup opens instantly. It reuses the file the browser already chose and cached for the tile, rather than fetching a bigger one. For a larger picture in the popup, point pic.src in block 5 of js/main.js at a separate file instead.
11. The ribbon, parallax and motion
The ribbon
The gold strip under the hero slides sideways forever, and it is pure CSS. It works by holding two identical copies of the list side by side and sliding the pair left by exactly half their width. When the animation restarts, the second copy is sitting precisely where the first began, so the join cannot be seen.
That is why the list appears twice in the HTML. If you change the words, change both copies, or the ribbon will stutter every time it loops. The second copy carries aria-hidden="true" so a screen reader only reads it once.
Speed is the 44s in the animation line on .ribbon-track. Larger is slower.
Parallax
The photograph band scrolls slower than the page. It is done in JavaScript rather than CSS because the CSS way, background-attachment: fixed, does nothing at all on an iPhone.
The picture inside the band is 140% of the band's height and pulled up 20%, which leaves 20% of slack above and below. The script never moves it more than 18% of the band height, so an edge can never appear. To make the movement stronger, raise DRIFT in block 4 of js/main.js, and raise the height and top on .band-pic to match.
To turn it off, delete the data-parallax attribute from <div class="band-pic">. The photograph then sits still and nothing else changes.
People who ask for less movement
Anyone whose computer is set to reduce motion gets a still ribbon, no parallax and no smooth scrolling. The page checks prefers-reduced-motion in the stylesheet for the ribbon and once at the top of the script for the rest.
12. Without JavaScript
Everything works. This is worth knowing because it is unusual.
| Feature | With JavaScript | Without |
|---|---|---|
| The tabs | Change panel on click | Change panel on click. They are radio buttons and CSS. |
| The ribbon | Slides | Slides. It is a CSS animation. |
| The form | Sent in the background, replaced by a thank you | Posts normally, send.php answers with a page of its own |
| Field errors | Printed under the field that is wrong | Listed on the answer page |
| Recent work | Opens a popup | Photographs still show, they simply do not enlarge |
| Parallax | The photograph drifts | The photograph sits still |
| Header | Turns solid on scroll | Stays transparent, which is still readable over the hero |
| Back to top | Appears on scroll | Not shown, nothing is lost |
Nothing on the page is hidden by CSS and revealed by script, which is the usual reason a page arrives blank when a script fails to load.
13. Browsers
Chrome, Edge, Firefox and Safari, current versions and a good way back. There is no build step, no framework and no library, so there is nothing to keep up to date.
- WebP images fall back to JPEG on their own.
aspect-ratiokeeps pictures from squashing. An older browser that does not know it still shows the picture.- The tabs use the general sibling selector and
:checked, both of which have worked for well over a decade. - The date field falls back to a plain text box where it is not supported, and
send.phpaccepts what it sends. - The script is written in the older JavaScript style on purpose, so it runs without being compiled.
14. Troubleshooting
| What you see | What it is |
|---|---|
The page is blank after editing fillup.php | A missing quote mark or semicolon. Undo your last change. Use straight quotes, not curly ones. |
| A tab you added does nothing | The two CSS rules that list the tab numbers were not extended. Section 5. |
| The tabs open with nothing showing | The input carrying checked was deleted. Put it on whichever input is now first. |
| The ribbon jumps every few seconds | The two copies of the list no longer match. Section 11. |
| The form says it sent, but no email came | Check storage/. If the brief is in the CSV, the form is fine and the email is the problem. Section 8. |
Nothing appears in storage/ | PHP cannot write there. Set the folder to 755, or 775 if your host needs it. |
| The email arrives in spam | Set $FROM to an address on your own domain, then turn on SMTP. Section 8. |
| The confirmation email shows the wrong date | {date} was used where {eventdate} was meant. Section 6. |
| The page jumps about while it loads | An image whose width and height attributes do not match the real file. Section 9. |
| A brief from a real person never arrived | Look in storage/trapped-*.csv. If they are in there, a spam trap caught them. |
| The CSV file downloads in a browser | Stop and fix this. Section 7. |
15. Licence
Use this template for personal, commercial or client work, on as many sites as you like. Do not resell or redistribute the template files themselves.
The photographs that came with it are placeholders and are not covered. Replace them before you publish.
Full terms: 7uptheme.com/license