Directory Theme class reference
Coupon Theme — Class reference
The 103 classes that make _cp behave differently from the shared Directory core. Generated from the theme source.
← Back to Coupon Theme features
AdminFeeds AdminFeeds.php
AdminFeeds — the coupon feed manager (_cp). A themed admin screen (rendered in the theme Chrome, like Admin\Bids / _ht\AdminReservations) listing every coupon feed the site pulls from, with a dry-run preview that fetches and parses a feed WITHOUT writing anything. Registered only under the Coupon profile (activeFork '_cp'); the nav entry is added in Admin\Chrome::sections() under the same gate. A feed here is ONE PER NETWORK, not one per merchant — that is the shape the affiliate networks actually publish (a single Awin or Rakuten response carries hundreds of advertisers), and it inverts the comparison theme's model, where a feed is configured against a merchant that already exists. Merchants are therefore DISCOVERED from feed rows rather than configured ahead of them, and each becomes a listing_store term ({@see StoreTax}) that the Stores screen then owns — the feed supplies the name, a human supplies the logo and blurb. The preview counts how many of a feed's merchants already exist as stores so that cost is visible up front. Fetching and parsing reuse the comparison pipeline wholesale ({@see \Ppt\Compare\FeedFetcher} streams a large, possibly gzipped file to disk; {@see \Ppt\Compare\FeedParser} pulls rows one at a time). Only the field mapping is coupon-specific, and that lives in {@see FeedNetworks}. Preview only, deliberately: the write path (upsert on source id, merchant find-or-create, and the tombstone step that expires codes which drop out of a feed) is the next phase and belongs with Compare\Ingest's run-logging, not bolted onto a screen. Nothing here creates, edits or deletes a listing.
AwinApi AwinApi.php
AwinApi — the live Awin Promotions API as a feed source (_cp). Every other source this screen handles is a file you can download: a URL goes to download_url(), bytes come back, {@see \Ppt\Compare\FeedParser} reads them. Awin's promotions endpoint is not that. It is a POST, it carries a JSON filter body, it wants a bearer token in a header, and it hands back one page at a time — none of which a GET fetcher can express. Rather than teach the whole pipeline about HTTP verbs, this walks the pages itself and writes the concatenated result to a temp file in exactly the shape the sample feed already has. Downstream nothing changes: the same json parser reads it, the same field map in {@see FeedNetworks} reads the records, the same upsert and the same tombstone run. The API becomes a file, and the file is the thing the pipeline already knows how to eat. publisher in the path is SINGULAR. Awin's transactions endpoints use publishers plural, which is a trap worth stating rather than rediscovering: the plural form returns a 404 that reads like a bad token.
BalmCard BalmCard.php
Balm's own coupon card (Health & Beauty Coupons, Coupon fork). The row from Balm's home-page code stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. It is the widest of the three Health & Beauty cards: a bordered left panel (monogram, coupon kind, and the use-count as a badge), the body, and a right panel holding the code and its button. TWO ELEMENTS OF THE APPROVED MOCKUP ARE DELIBERATELY ABSENT, because a live coupon cannot supply either and a home-page-only flourish that vanishes in search defeats the point of one shared card: - the per-row description paragraph. {@see CardData} carries no description, from a post or from block fields. - the "96% worked" success percentage. Nothing derives it. The badge SHAPE is kept and filled from note/proof — the use-count, which {@see \Ppt_cp\Redeem} does supply — so the left panel still closes on a figure. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
BellhopBox BellhopBox.php
Bellhop's own search box (Travel & Hotels Coupons, Coupon fork). Coupon browse pages carry no heading strip, so without this a design has no search of its own over its own results. This is the masthead's field put back at the top of the results it filters, in Bellhop's hand. Store-aware: inside a store it says so, and the store term rides along in the hidden fields so the search stays where the visitor is.
BellhopCard BellhopCard.php
Bellhop's own coupon card (Travel & Hotels Coupons, Coupon fork). The row from Bellhop's home-page code stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. The lead column is a monogram ROUNDEL — the shape the approved mockup carries — showing the store's logo when it has one and its initials when it does not. The discount is therefore not shown as its own element: the offer line carries it, so a live coupon that states its figure in another currency cannot contradict a second figure beside it. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
BiscuitCard BiscuitCard.php
Biscuit's own coupon card (Pets & Pet Care Coupons, Coupon fork). The row from Biscuit's "Latest coupons" stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself: a 12px-rounded white card with a green-tint monogram and an oat kind chip on the left, the store in green over a bold grotesque title, an expiry / checked / used line, an optional description, and a rounded "Show code" button with a dashed code stub under it (deals get a charcoal button). The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
BreezeBox BreezeBox.php
Breeze's own search box (Summer Coupons, Coupon fork). Coupon browse pages carry no heading strip, so without this a design has no search of its own over its own results. This is the masthead's field put back at the top of the results it filters, in Breeze's hand. Store-aware: inside a store it says so, and the store term rides along in the hidden fields so the search stays where the visitor is.
BreezeCard BreezeCard.php
Breeze's own coupon card (Summer Coupons, Coupon fork). The row from Breeze's home-page code stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. Its shape is the one the approved mockup replicated from the reference: the store's LOGO in the left column where other coupon designs put a discount figure, then the store name, the offer line and the trust row, with the reveal button on the right. The discount figure is therefore not shown as its own element at all: on this design the offer line carries it ("20% off garden furniture"), exactly as the reference does. An earlier pass printed fig_big as a pill before the title when the title did not repeat it, which duplicated the discount whenever a live coupon stated it in another currency - "£50" beside "$50 off orders over $299". The card omits rather than repeats. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
ByteCard ByteCard.php
Byte's own coupon card (Tech & Electronics Coupons, Coupon fork). The centred logo card from Byte's "Top recommended deals" grid, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. Top row: the end date on the left, the usage count on the right. Then the store's round MONOGRAM (or its logo), the store name in teal small caps, the offer, the checked line, a pill button and, when the store has a page, a small "All <store> coupons" link. The figure is not drawn separately — a live coupon's title already carries it — so home page and search show the same card. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
CabanaBox CabanaBox.php
Cabana's own search box (Summer Coupons, Coupon fork). Coupon browse pages carry no heading strip, so without this a design has no search of its own over its own results. This is the masthead's field put back at the top of the results it filters, in Cabana's hand. Store-aware: inside a store it says so, and the store term rides along in the hidden fields so the search stays where the visitor is.
CabanaCard CabanaCard.php
Cabana's own coupon card (Summer Coupons, Coupon fork). The row from Cabana's home-page code stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. Its shape is the one the approved mockup replicated from the reference: the store's LOGO in the left column where other coupon designs put a discount figure, then the store name, the offer line and the trust row, with the reveal button on the right. The discount figure is therefore not shown as its own element at all: on this design the offer line carries it ("20% off garden furniture"), exactly as the reference does. An earlier pass printed fig_big as a pill before the title when the title did not repeat it, which duplicated the discount whenever a live coupon stated it in another currency - "£50" beside "$50 off orders over $299". The card omits rather than repeats. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
CardData CardData.php
The normalised coupon card — the one shape every design's card renderer reads. Two producers, one shape. That is the whole point: a design writes its card markup once and it serves the search page, the store page and its own home-page code list, instead of the fork drawing one card on search and the design drawing a different one on the home page. fromPost() a real listing → live data, a post id, reveal-in-place buttons fromFields() a block's flat item{N}_* fields → curated copy, builder field markers, plain links (there is no post to count a reveal against) The figure / checked / ends / peek logic lives here rather than in a card renderer because it is editorial policy, not styling: what counts as a readable discount, what a trust line is allowed to claim, how much of a code may be shown for free. A new design inherits all of it by calling fromPost(); it only decides where the values sit on the card.
Cards Cards.php
Which card the active coupon design draws, and the shared plumbing around it. Every coupon design owns its own search card — the results card AND the search box above them — by implementing {@see \Ppt\Designs\Contracts\HasSearchCard}. This is the one place that asks the design which classes those are, and the one place that decides what happens when it does not answer: results card → the design's, else {@see DefaultCard} (the fork's own deal row) search box → the design's, else none (coupon browse pages carried none before) So a design added tomorrow gets its own card by declaring two class names, and a design that declares nothing renders exactly what it renders today. Neither the search template nor the design's home-page block knows which renderer won. Three things are shared rather than left to each design, because they are page mechanics rather than looks: • the results-grid rules — Bootstrap's row-cols sizes every direct child, so a full-width card has to opt out of the column width whichever design drew it; • the redeem pop-up ({@see RedeemModal}) — one delegated listener firing the same nonce-checked {@see \Ppt_cp\Redeem::AJAX} beacon the single page uses, so a reveal counts identically wherever it happens, including on AJAX-refreshed rows; • the "did it work?" prompt the pop-up asks, which feeds the store page's live feed ({@see \Ppt_cp\CodeHistory}); • the once-per-request printing of all of the above. A design's card opts into the pop-up by echoing {@see RedeemModal::attrs()} on its action element and carrying data-ppt-deal="" on its root. Nothing else is required of it — the pop-up is drawn from the attributes in that blob, so every design collects the signal and a design written later collects it for free.
CategoryArt CategoryArt.php
CategoryArt — a live picture for a coupon site's category tiles (_cp). The three coupon designs whose category row is a PHOTO row (Tally, Perk, Pleat) ship a square shot per tile: folded jeans for Fashion, a laptop for Tech. On a live site {@see \Ppt\Blocks\Support\Categories::autoFill()} already replaces the tile's name, count and link with the site's real listing_category terms — but not the photo, because its only two picture sources are an admin-chosen term image and the newest listing's own photo, and a coupon site has NEITHER: a code imported from an affiliate feed carries no attachment at all. The tile therefore ended up showing a photograph of jeans captioned "Health & Beauty". A coupon site does have one real, plentiful picture: the logos of the shops whose codes are filed under that category. 1,282 of the 1,303 stores on the reference install resolve one. So a tile with no photo of its own is filled with a mosaic of the logos of the biggest shops in that category — which is also what the tile is actually promising ("284 codes in Fashion" = these brands). The fallback chain, most specific first: 1. an admin-chosen category image → used as the photo, untouched (Categories); 2. the newest listing's own photo → used as the photo, untouched (Categories); 3. store logos in that category → this class, a mosaic; 4. nothing at all → the design's own art stays, so a brand-new coupon site still has a presentable home page. Same gates as its siblings {@see LiveCodes} and {@see LiveGuides}: it runs from Blocks\Renderer via LiveCodes::apply(), so the showroom, the demo-default preview and the builder canvas all keep their curated photography. Escape hatch: ppt_cp_category_art (per term, return an array of URLs or [] to switch it off).
CategoryIcons CategoryIcons.php
CategoryIcons — a retail glyph for a live category tile (_cp). The icon-tile category rows (Breeze, Sorbet, Cabana, Loom, Offpeak, Bellhop, Waypoint) are the icon twin of the problem {@see CategoryArt} solves for photos. Support\Categories::autoFill() gives every tile the site's real term name, count and link, but it only writes item{N}_icon when the term has an ADMIN-CHOSEN icon — and a site imported from a feed has none. The design's own glyph therefore survived beside a category it knows nothing about: a swimsuit captioned "Health & Beauty", a barbecue captioned "Clothing", a ferry captioned "Pharmaceuticals". Two things make this harder than the photo case: 1. Each block ships its OWN small icon set (Breeze has swim/sun/grill/plane/…), not the shared 138-glyph library — and that library is travel and lifestyle vocabulary anyway, with no beauty, health, clothing, electronics or pharmacy glyph in it. There was nothing to match a real retail taxonomy against. 2. On a miss, four of the blocks fell back to reset(self::ICONS) — the FIRST icon in their own set — and three rendered nothing at all. So an owner who set a term icon by hand got every tile wearing the same wrong glyph, which is worse than not setting one. So this class carries a retail vocabulary of its own, matches a live category name to it, and gives the blocks one lookup that can render a key they do not own. Contract, as ever: sample contexts keep the design's curated glyph untouched (the pass runs from LiveCodes, behind PreviewMode); a term with an admin-chosen icon keeps it; a live term with no icon gets the matched retail glyph; anything unmatched gets the neutral price tag rather than a confident wrong picture. Escape hatch: ppt_cp_category_icon (per term name).
CodeHistory CodeHistory.php
Code history — the dated record behind the store page's "Code activity" section (_cp). The fork already counted reveals and clicks, but only as running totals in post meta (coupon_reveals / coupon_clicks) plus one site-wide per-day option. Neither can answer "what did this store's code activity look like over the last year", and the alternative to answering it honestly is drawing a curve out of nothing. So this keeps one row per event, dated, exactly as {@see ListingViews} already does for page views — the same storage shape, the same UTC convention, the same zero-filled-series read. Two families of event share the table: reveal / click a shopper used the code. Logged from the ppt_cp_redeemed action {@see Redeem::record()} fires, so every reveal path — the deal row on search and store pages, the single page — counts identically and nothing had to be re-plumbed. works / fails a shopper reported back. This is NEW: the theme had no worked/didn't-work signal at all, only star reviews. The report is offered the moment a code is revealed, which is the one moment the question is real, and is what the live feed reads. Reports are deduped per shopper per coupon ({@see DEDUPE}) so the feed reflects distinct shoppers rather than a refresh. A shopper is their user id when logged in, and otherwise a salted hash of their IP — the raw address is never stored. Nothing here invents a figure. A store with no rows has no section ({@see StoreActivity}), which is the honest answer for a site that only went live last week.
CodeWindow CodeWindow.php
The code box — where a shopper reads the code, while the tab they came from goes to the merchant (Coupon fork). This exists because of one thing browsers will not let a page do: a tab opened from a click is brought to the front, and nothing — blur(), focus() on the opener, reusing a held handle — reliably prevents that. So it stops fighting: the tab that is GOING to be focused is the one given the code, and the tab left behind is sent to the affiliate link. The shopper reads exactly what they clicked for, with the store loaded behind. What that opened tab shows is the page the shopper was already on, rendered again, with the box over the middle of it. It used to be a document of its own — a lone card on an empty grey ground, no header, no site, nothing they recognised — and answering "show me the code" with a blank page is a poor trade for the code. The extra render buys the box a page to sit in front of, and costs nothing else: the site behind it is the site they were reading a second ago. Three decisions make that survive contact with a real browser: 1. A real URL, not a window written into by script. The opener navigates away a fraction of a second later, and a document built by the opener would lose every event listener the moment it did. This page owns its markup, script and nonce. 2. Opened as a plain tab with no feature string. A pop-up blocker's target is a window given a size and chrome flags, or one opened with no click behind it. An earlier version asked for width=470,height=640,menubar=no…, which is precisely the shape that gets blocked. Featureless and inside the gesture, this is what any target="_blank" link already does. 3. Nothing depends on that tab being allowed. When it is refused, the opener sends ITSELF here instead ({@see SOLO}) — a same-tab navigation, which nothing can block. The shopper still gets the code, on this page, with the store one click away; all they lose is the second tab. A coupon site whose codes only appear if the browser cooperates is a coupon site that does not work. {@see PUSH} is the fourth case, and the newest. A DEALS-grid tile carries a link into this site rather than a merchant URL — the block that drew it has the listing's permalink and nothing else — so the opener cannot send itself anywhere useful. It says so, and this page, which does know the merchant, drives the opener there instead. Same outcome, decided one page later. The reveal is counted HERE, server-side. A page load cannot be blocked the way a beacon fired during unload can, so this is the more honest count as well as the simpler one. Never indexed: this URL is a fragment of an interaction, not a page of the site. The page WITHOUT the parameter is the real one and stays indexable.
CuffBox CuffBox.php
Cuff's own search box (Fashion & Clothing Coupons, Coupon fork). Coupon browse pages carry no heading strip, so without this a design has no search of its own over its own results. This is the masthead's square white field put back at the top of the results it filters, in Cuff's hand: hard corners, a black Go block. Store-aware: inside a store it says so, and the store term rides along in the hidden fields so the search stays where the visitor is.
CuffCard CuffCard.php
Cuff's own coupon card (Fashion & Clothing Coupons, Coupon fork). The code row from Cuff's home-page "Best codes right now" list, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. Square-cornered and monogram-led, as the approved mockup: a solid coloured square carrying the store's initial (or its logo when the store term has one) on the left, the store name in tracked small caps, the offer title and a meta line of checked · used · ends, then on the right a black "Show code" block with a dashed cobalt scissors tab beside it for codes, or an outlined "Get deal" for deals. The monogram colour is dealt from a hash of the store name (SWATCHES), so a live store gets a stable colour without any field and a curated row the same; the design's cashback tiles use the same hash so a store wears one colour everywhere. Revealed state: the button itself becomes the dashed cobalt code box (.is-shown) and the tab (data-ppt-peek) is removed by the shared listener. The tab never carries any character of the code. The CSS is scoped to .ppt-cpcard--cuff, never to the block section, because the same rules have to hold on a search page where that section is absent.
DashCard DashCard.php
Dash's own coupon card (Sports & Fitness Coupons, Coupon fork). The row from Dash's "Latest coupons" stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself: a white card with an orange left rule, a charcoal square monogram and a tinted kind pill on the left, the store in spaced orange caps over a bold title, an expiry / checked / used line, an optional description, and an uppercase "Show code" button with the code stub under it (a deal takes the charcoal button). The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
DealRow DealRow.php
DealRow — how a coupon looks in a list (Coupon fork). This was the markup itself until 2026-09-08. It is now the name the rest of the theme calls that markup by, while the drawing is done by whichever card the active design owns: Ppt_cp\Cards\Cards which card the design declared (or the fork default) Ppt_cp\Cards\CardData the listing, normalised into the shape cards read Ppt_cp\Cards\DefaultCard the fork's own deal row — this file's former contents Ppt_cp\Cards\TallyCard Tally's voucher card, its home page's and its search page's, one definition The change is why: every coupon design now owns its own search card, and a design added later gets one by naming two classes rather than by having this file re-measured against its home page. See inc/_cp/Cards/README.md. The seam is kept because the shared search template calls DealRow::render() by name (inc/Directory/templates/search.php, the $ppt_card closure) and PROOF_FLOOR is read by {@see LiveCodes}. Both keep working; neither has to know a design was involved.
DefaultCard DefaultCard.php
The fork's own coupon card — what a coupon design gets when it declares no card of its own (Coupon fork). A coupon is not a photograph. The photo card the other forks use reserves its top two-thirds for an image a coupon feed never supplies, which pushed the one thing a shopper is looking for — the discount — off the card entirely. This is the layout every coupon site converges on for the same reason: the discount as a large figure on the left, the store and the offer in the middle, the action on the right. The action is the point of it. Codes and offers have no page of their own, so the code is revealed IN PLACE — no interstitial, no navigation — by the shared listener in {@see Cards}. A curated row (no post id) has nothing to reveal or count, so its button is a plain link instead. Every trust signal is derived from something real by {@see CardData}, and is omitted rather than invented when the data is absent. Colours come from the design's --ppt-* tokens, never hardcoded, so a design without its own card still gets a row in its own palette.
DemoAccounts DemoAccounts.php
Coupon — what the DEMO member walks into. A coupon site has one kind of member, so the home-demo offers a single "members area" login (the plain test member). Empty, that hub shows nothing worth looking at, so this lends the member three of the seeded demo coupons as their own submissions and saves four more as favourites — the shape a member's hub takes once they have used the site. The rows come from _cp\DemoFeed (Support\Demo-marked listings); DemoLogin lends them and hands them back at purge exactly as it does for any typed member. Gated to the coupon profile by Theme::boot()'s fork-gating (this is Ppt_cp*).
DemoCards DemoCards.php
DemoCards — what a coupon search page shows in the showroom (_cp). A coupon site's search results are COUPONS. Everywhere else in the theme a design preview borrows the niche's curated demo listings, and those are product-shaped by definition — a photograph of a thing, a category chip and a price. Rendered through the shared listing card on this fork they read as a product catalogue, which is the one thing the coupon search surface must never look like: products have their own page ({@see Products}), and the search page is where codes and deals live. So on _cp the showroom builds its own rows, in the coupon card's own shape ({@see Cards\CardData}), from two sources in this order: 1. The previewed design's own code list. Whatever its home page shows, the search page shows — same copy, same store names, same card class. That is the point of {@see Cards\Cards}: one card definition, home page AND search. 2. The niche's merchants, as offers. A design ships a handful of curated rows (Tally ships five), which is a thin results page. The niche pool behind every other fork's demo search already names ~19 shops with a headline saving each ("Up to 40% off"), so those become the rest of the page — the store's name, its saving as the figure, its blurb behind the card. Nothing here ever runs on live data. The search template only asks for these rows while it is in preview AND the site has no coupons at all ({@see TypeFilter::hasAny}), so a real catalogue always answers for itself. A merchant that carries no shipped logo gets a monogram, exactly as an unknown merchant from a real feed does.
DemoCategory DemoCategory.php
DemoCategory — the showroom's categories, and the pages behind them (_cp). The sibling of {@see DemoStore}, built for the same reason and on the same rules: a coupon install seeds no listings at all, so /categories/ showed its empty state in a design preview while the rest of the design was full of offers, and there was nothing to click. WHERE THE CATEGORIES COME FROM — and this is the part worth keeping: they are NOT a fixed shipped list like the twenty stores are. Every coupon niche already ships nineteen categories through {@see SampleListings}, and they are the previewed design's OWN subject: Loom (fashion) has Denim, Knitwear and Coats & outerwear; Offpeak (travel) has Flights, Rail travel and City hotels. Reading them from the niche means the categories page matches the design being previewed instead of showing a shop's departments under a travel theme, and it means a new niche gets its categories for free — nothing to add here when one is added. THREE GATES, all required — identical to DemoStore, for identical reasons: 1. a REAL term with that slug always wins; 2. {@see Preview::isPreview()} — a live site 404s an invented URL as before; 3. the slug must match one of the previewed niche's categories. Nothing here writes: no terms, no posts, no options.
DemoFeed DemoFeed.php
DemoFeed — the Coupon Theme's demo data, and the sample feeds it is built from (_cp). 10 stores, 10 coupons, 10 deals and 6 products. The stores are real terms carrying their shipped logo, website and blurb; every coupon, deal and product is filed under one of them, so each store page has something on it and no store appears from nowhere. Two jobs, deliberately in one place: 1. A downloadable example of the format. A site owner setting up their first feed has nothing to compare their file against. This writes a real, valid CSV in the shape {@see FeedNetworks} expects, so "what should my file look like" is answered by a button. 2. Installable demo content. The same files imported through the ordinary pipeline — no special path, so what an owner sees demoed is exactly what their own feed will do. Reached from Feeds ▸ Sample feed, from Sample Data, from the setup wizard's "Add sample listings" box and from a design switch — the last three through {@see \Ppt\Tools\SampleData}, which hands the coupon profile here instead of seeding its generic niche rows. Everything it creates is TAGGED ({@see FLAG_META} + {@see Demo}) so removal is exact. That is the whole design constraint here: demo content that cannot be removed cleanly is worse than no demo content, because it silently mixes with a real catalogue. Remove deletes only what carries the tag — a coupon imported from a real network is never touched, and a store the owner already had is used but never restyled or deleted. The rows are CURATED, not generated: ten fixed shops means every offer can be worded for the shop it belongs to, so a travel site never says "free delivery" and a pizza chain never offers "15% off skincare". The file is byte-identical on every build, so a re-import updates in place instead of churning.
DemoStore DemoStore.php
DemoStore — /stores/<slug>/ for a shop that exists only in the showroom (_cp). A coupon install seeds no stores ({@see StoreSeed::uninstall}), so in a design preview the Stores index draws the twenty shipped shops as dressing rather than terms ({@see \Ppt\Frontend\Pages::demoStoreRows}). Those tiles have to lead somewhere: the archive rewrite /stores// exists on this profile, but the TERM behind it does not, so every tile pointed at the deal archive instead — click Nike, land on a page of everybody's offers. That is a dead end dressed up as a link, and the one rule this index has always had is that it must never dead-link. So the slug is served: the request is claimed before WP can 404 it, the page renders through the ordinary search template with the SAME store header a real store gets ({@see StoreTax::renderHead}), and the results are that shop's own showroom offers ({@see DemoCards::storeCards}). Nothing is written — this is demo TEMPLATE, not demo DATA, the distinction the whole no-demo-content rule on this fork turns on. THREE GATES, all required, in this order: 1. a REAL term with that slug always wins — the moment an owner has a store, its own archive answers and nothing here fires; 2. {@see Preview::isPreview()} — a live site 404s an invented URL exactly as before, so no demo shop is ever a public page on somebody's real site; 3. the slug must match a SHIPPED store — an unknown slug still 404s.
DiodeCard DiodeCard.php
Diode's own coupon card (Tech & Electronics Coupons, Coupon fork). The centred, portrait card from Diode's tabbed coupon grid, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. The lead is the store's WORDMARK — its name set in the display face inside a ruled box, or its logo when the store has one — over a red discount figure, the offer, the checked line and a full-width button. The button becomes the dashed code box on reveal, so there is no separate tear stub on this design. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
Entitlements Entitlements.php
Coupon-site entitlements — what a membership plan grants a BUSINESS member on the Coupon Theme, and the gate that enforces it. WHY THIS EXISTS. A coupon site has two completely different supply models and the theme now serves both. A feed-driven voucher site imports its codes from Awin or a crawl and has no members posting at all. A LOCAL OFFERS site is the opposite: the supply IS the businesses, and the thing being sold is the right to post. This class is the second model's meter — "an annual tier that allows 12, 50 or unlimited coupons" — expressed as one row on core's {@see \Ppt\Account\Entitlements} registry so it is edited in the ordinary plan grid and read through the ordinary {@see \Ppt\Account\Membership} resolver. SLOTS, NOT SUBMISSIONS. The row is a PERIOD_NONE quota: a standing figure compared against a live count, never an allowance that is used up. "12 coupons" therefore means twelve live at once, and an offer that expires or is taken down frees its slot the moment it does. That is both what a business owner assumes the words mean and the only reading with no year-boundary to get wrong — there is no bucket to reset, no renewal date to align a counter to, and nothing to reconcile when a plan changes mid-term. It also means consume() is never called here: {@see liveCount()} IS the usage figure, derived from the posts themselves, so it cannot drift out of step with what is actually on the site. WHAT OCCUPIES A SLOT. Published, pending and scheduled coupons that have not expired. - pending counts because a submitted offer awaiting approval is a claim on a slot: leave it out and a member could queue fifty submissions and have them all land at once when an admin approves the batch. - draft does NOT count. An unfinished coupon nobody has submitted is not an offer on the site, and counting drafts would let a member paint themselves into a corner with abandoned ones they would then have to hunt down and delete. - an EXPIRED coupon never counts, whatever its status. This is the point of slots. THE GATE FAILS OPEN, like _so's and the dating fork's: it only refuses once some ENABLED plan actually expresses a limit ({@see gated()}). A coupon site that has not configured memberships at all behaves exactly as it did before this class existed, which is what stops the feature landing on existing installs as a lockout.
Expiry Expiry.php
PPT — listing expiry / "Listing lifetime". The Settings ▸ Listings "Listing lifetime" (days) governs how long a listing stays live. 0 = never expires. A value > 0 stamps an expiry timestamp on each listing when it's published; a daily cron then runs the configured "On expiry" action (listings_expiry_action: nothing / draft / pending / trash) once the time is up. The expiry timestamp meta is SHARED with the pricing-plan expiry (PricingPlans::LISTING_EXPIRES_META). A listing that carries a pricing plan is governed by that plan's own duration (set in the editor / at checkout), so the global lifetime only applies to listings WITHOUT a plan. Either way, the cron here enforces whatever expiry timestamp a listing ends up with, and both editors show the time remaining.
FeedIngest FeedIngest.php
Coupon feed INGEST — the run that turns feed rows into listings. fetch → parse → normalise → upsert → tombstone, mirroring {@see \Ppt\Compare\Ingest} because coupons need the same two freshness guarantees a price feed does: • every listing this run touched is stamped with the run id; • after the run, every listing from this network that the run did NOT touch is expired — a network never says "this code is dead", the code simply stops appearing, so absence IS the expiry signal. Without the second half a coupon site fills up with codes that quietly stopped working, which is the one thing that destroys trust in it. Identity is ppt_cp_source_id + ppt_cp_source_network, so a nightly re-run updates in place rather than duplicating. The whole network's existing ids are read once into a map at the start of a run, not looked up per row — a meta_query per row is what turns a 500-row feed into a timeout. Coupons are listings ({@see Listings::TYPE}), distinguished by coupon_record_type: code when the row carries a voucher code, offer when it is a link-only promotion. product is reserved for the product-feed phase, which is not wired here. Stores need no handling: writing coupon_merchant files the listing under the matching listing_store term and creates it on first sight ({@see StoreTax}). Runs are time-budgeted and resumable. A coupon feed is hundreds of rows and normally finishes in one pass, but a run that would exceed the budget saves its offset and reports done => false rather than dying half-applied — and a half-applied run must NOT tombstone, or every row it never reached would be expired.
FeedNetworks FeedNetworks.php
Coupon feed NETWORKS — what each affiliate network calls the same seven things. Every network publishes the same idea (merchant, title, discount, code, destination, expiry, terms) under different names and in a different container, so the only thing that varies between them is a field map. That map lives here; nothing else in the coupon import knows a network exists. Two things no network gives us cleanly, and which {@see discount()} therefore has to derive from text: • Rakuten has a promotiontypes taxonomy but no numeric amount; • Awin has neither — the discount is only ever stated in the offer title. So the same parser runs for every network, seeded by a type hint where one exists.
Gallery Gallery.php
Single-listing gallery styles. The admin picks one of four layouts on the Design ▸ Listings tab (stored in the ppt_design option via Branding), and the single-listing template renders the chosen layout for the listing's images. standard — large hero photo + thumbnail strip (click to swap) [default] grid — all photos in a tiled grid (first one featured) carousel — a swipeable/scroll-snap slider with prev/next tall — full-width photos stacked vertically (good for portraits)
GarlandCard GarlandCard.php
Garland's own coupon card (Gifts & Flowers Coupons, Coupon fork). The row from Garland's "Latest coupons" stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself: a square-cornered white card with a round plum monogram and a small-caps kind label on the left, the store in spaced small caps over a serif title, an expiry / checked / used line, an optional description, and a plum "Show code" button with a gold-edged code stub under it. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
Geocoder Geocoder.php
Bulk geocoder — fills in lat/lng for existing listings that have an address but no coordinates (e.g. listings created before the editor's map picker existed), so they get a precise map marker and the "Distance from me" feature. Uses the site's Maps provider (Settings ▸ API keys): Google / Mapbox geocoding APIs (their key), or OpenStreetMap Nominatim (keyless, rate-limited to ~1 req/sec with an identifying UA). A small box on the PPT Listings screen runs it in batches over AJAX. Listings that can't be geocoded are flagged (_ppt_geo_failed) so they aren't retried forever.
HomeRail HomeRail.php
HomeRail — the widget column a coupon design's home page draws beside its code list. The Gifts & Flowers designs (Posy / Garland / Sprig, 2026-09-11) were approved with the fork's own sidebar WIDGETS as their right-hand column — the same six types the search page draws, skinned per design — rather than a hand-built <aside> of flat fields. This helper is the one place that list is resolved so the three code blocks share the behaviour instead of copying it: 1. the owner's saved home rail ({@see Widgets::forContext()} for home) when they have placed anything there on PremiumPress ▸ Widgets; 2. otherwise the DESIGN's six, passed in by the block from its own editable fields. Nothing here writes the ppt_cp_widgets option: switching designs must not rewrite an owner's widget list, and a design's defaults are its own, not the site's. The widgets are drawn through their block classes directly rather than through a nested {@see \Ppt\Blocks\Renderer::render()} pass: the calling block is itself inside that renderer, whose per-block overlay state (LiveCodes::$slotIds) a nested pass would reset under it. The widget types read their own data (top codes, stores, expiring, categories) and need none of the overlays.
HostingRows HostingRows.php
The eight curated coupon rows the three Hosting & Software Coupons designs (Uptime / Stack / Ping, Coupon Theme, _cp) open with. One body, three heroes: the designs share this copy so a customer switching between them keeps the same sample rows, and so the copy is edited in one place. Each design's own ``_codes block still owns its fields — this is only the default text those fields are filled with. Row shape: store, initials, title, amount, unit, kind, code, verified, flag, note (the used-count — LiveCodes overlays a live site's real count into _note), ends, desc.
HuddleCard HuddleCard.php
Huddle's own coupon card (Sports & Fitness Coupons, Coupon fork). The row from Huddle's "Latest coupons" stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself: a square- cornered white card with a navy-tint square monogram and a small-caps kind label on the left, the store in spaced navy caps over a heavy grotesque title, an expiry / checked / used line (an imminent expiry in the kit red), an optional description, and a navy "Show code" button with a dotted code stub under it. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
JoltCard JoltCard.php
Jolt's own coupon card (Tech & Electronics Coupons, Coupon fork). The row from Jolt's "Featured discounts" stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. The lead column is a tinted FIGURE BLOCK — the discount as a big number over a small unit word — with the store's monogram as its stand-in when a live coupon states no figure. The action is a yellow "Show code" button with a dashed tear tab beside it; a deal is a solid blue "Get deal". The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
ListingActions ListingActions.php
Per-listing visitor actions shown on the single-listing section nav: - Add to favorites — toggles the listing in the member's saved list (user meta ppt_favorites); the account page shows them. Logged-in only; guests are routed to sign-in. - Report — flags the listing to the site admin. The reason is stored as a comment on the listing (type ppt_report, a custom approval status so it stays hidden from the front-end and the normal comment-moderation tabs) and also emailed to the admin. Reports are reviewed in the admin Comments screen's dedicated "Reports" tab. Open to guests and members.
ListingCard ListingCard.php
The canonical listing card — the single, theme-wide way a business/listing is shown as a card. Every directory surface (search results, single-page "related", and the live listing-grid blocks) renders through here, so a listing looks the same everywhere and the demo-vs-live data split lives in ONE place. Data-source agnostic: fromPost() maps a real listing_type record and fromSample() maps a curated demo row into the same normalized shape, which render() draws. The markup is built on the theme design tokens, so the one card automatically adopts each design's colours/fonts. Its CSS is enqueued site-wide (assets/css/listing-card.css) — the card only emits markup. Normalized shape: name, link, img, cat, rating, desc, desc_long, price, city, dist, featured (bool), highlighted (bool), badge (string), id badge is an optional ribbon label (e.g. "New") for callers that need one; when empty it falls back to "Featured" if featured is set.
ListingEditor ListingEditor.php
Listing editor — the shared brain behind the two listing-edit screens: - Admin : PPT-styled screen at ?page=ppt_listings&edit=<id> (Chrome shell), rendered by Admin\Listings when the edit param is present. - Member : front-end screen at /account/listing/<id>/ inside the Member Hub, rendered by Account\MembersPage for a listing the member owns. Both POST to admin-post.php (action ppt_listing_save) and run through the one save() below, so the field set, sanitising and persistence live in a single place. The mode (admin|member) decides which extras are honoured: admins get status, author, slug, featured/verified and the "edit only" custom fields; members get a friendly subset scoped to their own listing. Fields = core (title, description, category, tags, featured image, gallery, FAQ) + every field defined in the Custom Fields admin (Admin\Fields / option ppt_fields), stored as post meta keyed by the field key — which is what the single-listing template already reads.
ListingFaq ListingFaq.php
Single-listing FAQ. Reads per-listing FAQ items from the faq post meta (an array of ['q' => …, 'a' => …]). A live listing shows only its own saved FAQ — when it has none the section is omitted rather than filled with fabricated entries. Demo/preview mode supplies sample FAQ via DemoContent so a fresh design isn't empty. Populate a listing's own FAQ by saving faq meta, inject a site-wide default set via the ppt_listing_faq_default filter, or adjust the final list with the ppt_listing_faq filter.
ListingHours ListingHours.php
Business hours display for the single-listing sidebar. Reads the business_hours meta written by the listing editor (ListingEditor::HOURS_META) and renders a Google-Business-style day list with an "Open now / Closed now" badge computed in the site's timezone. Times are formatted with the site's time format.
ListingLocation ListingLocation.php
Single-listing "Location" section — the richer location block: the full formatted address, an interactive map (ListingMap, precise when the listing has coordinates), a "Get directions" button, and a "Distance from you" control (browser geolocation → straight-line distance to the listing). Self-contained: it prints its own scoped CSS + JS once, so single.php just calls render().
ListingMap ListingMap.php
The single-listing "Location" map. Uses the site's Maps provider setting (Settings ▸ API keys — the same ppt_maps_provider filter the search map reads). When the listing has stored coordinates (lat/lng, set by the editor's map picker) the map centres on them precisely and drops a named marker; otherwise it falls back to geocoding the address string: google / mapbox(fallback) → a keyless Google Maps embed osm → a Leaflet map (marker from coords, else Nominatim) Map libraries load from their CDNs — the owner-approved front-end map exception.
ListingPricing ListingPricing.php
Single-listing pricing plans. A listing owner enters one or more plans / packages on the submission form (a name, a price, and a short description); they're stored in the pricing_plans post meta as a list of ['name' => …, 'price' => float|null, 'desc' => …]. The LOWEST priced plan is mirrored into the searchable price meta (Ppt_cp\SearchPage::PRICE_META) by the editor on save, so the price range filter, the price sort, and the result-card price all work — that's the link between "enter your pricing" and "find listings by price". Prices are stored in the site's base currency; render() converts + formats via Ppt\Content\Currencies for the viewer's chosen currency.
ListingSections ListingSections.php
PPT — configurable single-listing sections. The admin (Design ▸ Listings ▸ "Listing page sections") gets a sortable list of on/off toggles for the optional feature blocks a listing page can carry — Booking, Business hours, Maps & Location, FAQ, Reviews. Turning one OFF removes it from BOTH the single listing page and the listing submission/edit form; the order of the list controls the order the (main-column) sections appear in. Config lives in the shared design option (Branding::OPTION = ppt_design) under the listing_sections key: { order:[keys…], enabled:{key:0|1} }. Anything not saved yet defaults to ON, in the catalog order — so existing sites are unchanged until the admin edits the list.
ListingTiles ListingTiles.php
The tile row at the top of the coupon listing form: Category and Store. Both were stacked chip-and-button fields — Category on the shared modal picker, Store on a bespoke inline panel that dropped a list of pills into the form. They are the same kind of decision ("which existing thing does this deal belong to"), so they now read as one row of tiles: icon, label, and either the chips that are set or the word "Select". Clicking a tile opens the SHARED category picker modal (assets/js/category-picker.js) — searchable, with a Clear All / Continue footer — so adding and editing choices is the same gesture on both. Store stays SINGLE-select (a deal belongs to one shop; StoreTax::assign writes one term), which the shared picker already supports via data-cat-multi="0". That also retires the inline store panel and its script: the modal replaces both. The two hidden-input shapes differ and the order in the markup is load-bearing: category[] an ARRAY — the picker writes one hidden input per chip, and the empty backstop can sit anywhere (PHP drops the empty member). coupon[store] a SCALAR — last value posted wins, so the "nothing selected" backstop is printed BEFORE the chip box, never after it.
ListingViews ListingViews.php
PPT listing analytics — records a "hit" every time a single listing page is viewed, and serves that data back to the listing owner as a graph in the member area (/account ▸ My listings ▸ the analytics icon). Each view stores the listing id, the timestamp (UTC), and — when the visitor is logged in — their user id + display name, so an owner can see who has been looking. Bots/crawlers are skipped so counts reflect real visitors. The owner (and admins) can read the analytics for a listing over the AJAX endpoint, which returns a daily series (for the chart), rolling-window totals (7/30/60/90 days) and a list of recent named viewers. Storage: a custom table {prefix}ppt_listing_views (dbDelta on init).
LiveCodes LiveCodes.php
LiveCodes — puts the site's real coupons into a design's code list (_cp). The coupon designs author their home-page code list as curated demo rows — "Northvale Home, 20% off, Verified 06:40 today, Confirmed by 214". That is right in the showroom and wrong the moment a site has its own feed running: the home page goes on advertising shops that do not exist while the real coupons sit on the search page. The shared {@see \Ppt\Blocks\Support\LiveItems} overlay already solves this for ordinary card grids, but it works off a generic vocabulary (title / image / price / location). A coupon row is none of those — its fields are item{N}_amount, _store, _code, _kind, _verified, _flag — so those keys pass straight through it untouched. This is the same idea in the coupon dialect, and it runs as one render-time pass in {@see \Ppt\Blocks\Renderer}, exactly like LiveItems and HowLinks: every coupon design's code list goes live at once, with no per-block wiring, and designs written later are covered for free. It follows LiveItems' contract to the letter: • sample context (showroom, demo-default preview, builder) → curated rows, untouched; • live site with published coupons → the real ones win; • live site with nothing published → curated rows stay, so a new install still has a presentable home page rather than a hole above the fold. The stats block ("Today's numbers") is overlaid too, and for a stronger reason than tidiness: shipped as demo copy it reads "Live codes 1,482 / Average saving £14.20" on a site with twenty coupons. Those are invented figures presented as fact about the site the visitor is on. Every value here is counted from real listings, and a figure with nothing behind it is dropped rather than guessed.
LiveGuides LiveGuides.php
LiveGuides — puts the site's real blog posts into a coupon design's guides row (_cp). Three coupon designs close their home page with an editorial three-up — "Saving guides" on Tally and Perk, "Saving notes" on Pleat. Each card is a photograph, a section tag, a title, a standfirst and a byline, authored in defaults() as curated demo articles. That is right in the showroom and wrong on a customer's site: the cards used to fall back to the SEARCH page, so a visitor who clicked "The price-drop refund almost nobody claims" landed on a list of coupons, and the site's own posts never reached the home page at all. {@see \Ppt\Blocks\Support\LiveItems} deliberately skips these blocks — they are category "text", not "listings", and filling an article card from the coupon pool would headline a discount code as a guide. This is the same overlay idea in the blog dialect, and it runs as one render-time pass in {@see \Ppt\Blocks\Renderer} right after {@see LiveCodes}, so every guides row goes live with no per-block wiring. It follows LiveItems' contract to the letter: • sample context (showroom, demo-default preview, builder) → curated rows, untouched (they link to the blog index — see indexUrl() — never to search); • live site with published posts → the newest posts win: title, permalink, excerpt, featured image, category, author, uploaded avatar, reading time; • live site with nothing published → curated rows stay, still linking to the blog index, so a new install has a presentable home page rather than a hole. These are homepage DESIGNS: the owner decides in the builder whether this row belongs on their page, and the render must not overrule that by deleting it. A post without a featured image keeps the design's own photograph for that slot; an author without an uploaded photo drops the avatar (every block prints it conditionally) rather than showing a stranger's face beside a real byline.
LoomBox LoomBox.php
Loom's own search box (Fashion & Clothing Coupons, Coupon fork). Coupon browse pages carry no heading strip, so without this a design has no search of its own over its own results. This is the masthead's pill field put back at the top of the results it filters, in Loom's hand: a sand pill with the sage Search button inset on its right. Store-aware: inside a store it says so, and the store term rides along in the hidden fields so the search stays where the visitor is.
LoomCard LoomCard.php
Loom's own coupon card (Fashion & Clothing Coupons, Coupon fork). The row from Loom's home-page "Today's trending fashion codes and deals" grid, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. Logo-led, as the approved mockup: the store's LOGO in the left column where other coupon designs put a discount figure, then the store name, the offer line and the trust row, with a sage pill button on the right that carries a blank dashed code tab with a scissors mark for codes, or reads "Get deal" for deals. The discount figure is not shown as its own element: on this design the offer line carries it ("20% off everything…"), and printing it twice would duplicate the discount whenever a live coupon stated it in another currency. The card omits rather than repeats. The tab never carries any character of the code. Revealed state: the shared listener removes the tab (data-ppt-peek) and the button becomes the dashed code box (.is-shown), reclaiming the tab's gutter. The CSS is scoped to .ppt-cpcard--loom, never to the block section, because the same rules have to hold on a search page where that section is absent.
Media Media.php
Listing images — the single, WordPress-native resolver for the PPT image system. A listing's photos are its WP featured image (primary) plus any image attachments parented to it (the gallery). No legacy ?imgid refs, no image-URL meta strings — everything flows through the media library.
MembershipLapse MembershipLapse.php
Stop paying, and your coupons expire. WHY THIS EXISTS. A membership tier that meters live coupon slots only means something while somebody is paying for it. Before this class, a lapsed business could not post anything NEW — Entitlements::slotLimit() resolves a lapsed member to the free tier (or to nothing) so the gate closed correctly — but every coupon they had already published stayed live for ever. Buy one year, post fifty offers, never renew, keep fifty offers. {@see \Ppt\Account\MembershipExpiry} says so in its own docblock: it emails, and "never changes their assigned plan". WHAT "EXPIRED" MEANS HERE. Exactly what it means for any other coupon on the site: {@see \Ppt\Payments\CheckoutFlow::expireListing()}, the same handler the listing expiry cron runs, honouring the owner's own Settings ▸ Listings ▸ "On expiry" choice — draft, pending, trash, "mark expired (keep page)", or nothing. This deliberately invents no second notion of expiry: a coupon that went because its own date passed and one that went because its business stopped paying end up in the same state, which is the state the owner asked for. It also never rewrites coupon_expiry. That date is the BUSINESS'S data — what they told shoppers about their own offer — and post-dating it to force an expiry would corrupt it and could not be undone on renewal. IT ENFORCES THE CEILING, NOT "LAPSED YES/NO". The rule is "no more live coupons than your plan allows", applied to whatever plan you are on NOW. That covers lapsing (the ceiling drops to the free tier's, usually 0, so everything goes) and it covers a DOWNGRADE for free: a business moving 50 → 12 has its excess retired instead of sitting over its allowance indefinitely. The newest offers are kept, because they are the ones most likely still worth showing. WHEN IT RUNS. On the existing daily membership-expiry cron, at a later priority than the emails, so there is no second schedule to keep alive and a member is told before (or as) their offers come down. A renewal clears the per-user flag so the next lapse is processed afresh, exactly as MembershipExpiry::rearm() re-arms its reminders.
MemberStore MemberStore.php
One store per business member — the shopfront a submitting business owns. WHY. {@see StoreTax} makes a store a real listing_store term, and the term is where the branding lives (logo, blurb, website). That was built for a site whose stores are big brands an ADMIN curates on the Stores screen. The moment businesses submit their own offers (memberListings, 2026-09-19) the question changes: which store does Rosa's Pizzeria's coupon belong to? The answer here is that a business member OWNS exactly one store term — theirs — and every coupon they post is filed under it automatically. That gives the business a real public page at /stores/rosas-pizzeria/ carrying their logo, their blurb and every live offer they have, which is the shopfront a local-offers site is selling. It also removes the whole class of problems the alternatives have: a member cannot file a coupon under somebody else's brand, cannot invent "Rosa's" / "Rosas" / "Rosa's Pizzeria" as three stores, and does not have to wait for an admin to add their business before they can post. OWNERSHIP IS RECORDED TWICE, deliberately: term meta _ppt_store_owner => user id the authority user meta ppt_cp_store => term id the index The term meta is what {@see ownerOf()} trusts; the user meta only makes "which store is mine" a single lookup instead of a meta_query on every editor render. {@see storeFor()} verifies the index against the authority on every read and repairs it when they disagree, so a term deleted underneath a member (or an admin reassigning one on the Stores screen) can never leave a member pointed at a store that is not theirs. ADMIN-OWNED STORES ARE UNTOUCHED. A store with no _ppt_store_owner is an ordinary curated/imported one and behaves exactly as it always has — which is what lets a single site run an imported catalogue and local businesses side by side.
OffpeakBox OffpeakBox.php
Offpeak's own search box (Travel & Hotels Coupons, Coupon fork). Coupon browse pages carry no heading strip, so without this a design has no search of its own over its own results. This is the masthead's field put back at the top of the results it filters, in Offpeak's hand. Store-aware: inside a store it says so, and the store term rides along in the hidden fields so the search stays where the visitor is.
OffpeakCard OffpeakCard.php
Offpeak's own coupon card (Travel & Hotels Coupons, Coupon fork). The row from Offpeak's home-page code stack, lifted out of the block so the same object is drawn on the search page, on a store page and inside the block itself. The lead column is the DISCOUNT FIGURE — the shape the approved mockup carries — with the store's monogram as its stand-in when a live coupon states no figure. The CSS is scoped to the card's own root class, never to the block section, because the same rules have to hold on a search page where that section is absent.
PageChrome PageChrome.php
PageChrome — the coupon browse pages' ground and full-width bands (_cp). Two things that must be true on EVERY coupon search and store page, whether or not it found anything: 1. The page ground. The coupon designs sit their code lists on a faint grey with white cards on top. This lived in {@see DealRow}'s stylesheet at first, which was wrong: that only prints when a deal row renders, so a store with no coupons — or an empty search — fell back to white and looked like a different site. 2. Full-width bands. The store strip and the store header sit inside the shared template's .container, but read as full-bleed bands: white, edge to edge, with a rule under them. They break out with symmetric negative margins and restate the container measure inside, so their content still lines up with the results below. The ground colour is a DESIGN token, --ppt-cp-ground. A design sets it and wins; a design that sets nothing gets a value mixed from its own line colour, which lands on the right shade for the palettes shipped so far. Nothing here is a fixed hex, so a dark coupon design restyles the page without touching this file.