R rawmark v0.3.1 Authoring Referenceverified against the current codebase, not a roadmap ↓ Download .zip ↓ Download SKILL.md
Start here

What Rawmark renders

A Rawmark "Code Page" is a standalone HTML document. No theme header, no theme footer, no theme stylesheet, no block-library CSS. What you write in the three panes, plus whatever WordPress plugins add through wp_head/wp_footer, is the entire page.

This is the whole document Rawmark builds. Nothing else is added, nothing is filtered:

<!DOCTYPE html>
<html lang="{site language}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{the page's title}</title>
  <!-- wp_head() fires here -->
  <style>
    /* your CSS pane */
  </style>
</head>
<body class="rawmark-page rawmark-page--{slug}">

  <!-- your HTML pane -->

  <script>
    /* your JS pane */
  </script>
  <!-- wp_footer() fires here -->
</body>
</html>

Your CSS loads in <head>, so there's no flash of unstyled content. Your JS runs after the DOM is parsed but before wp_footer. The <body> carries a per-page class, so page-scoped CSS never needs an extra wrapper <div>. There is no reset, no normalize, no theme default: a bare <h1> looks like a browser default <h1>. That's the trade for a genuinely clean document.

The three panes

HTML: body content only

No <!DOCTYPE>, <html>, <head>, or <body>. Rawmark supplies those. Start with a single root element:

<main class="lp">
  <section class="lp__hero">...</section>
</main>

CSS: plain rules, no <style> tag

Rawmark wraps it for you. No preprocessor, no build step. Write the CSS you want shipped.

JS: plain script, no <script> tag

It runs as a classic script at the end of <body>: the DOM is already parsed, so no DOMContentLoaded wrapper is needed. It is not a module, so import doesn't work; pull in a library with a <script src> tag in the HTML pane instead. Wrap your own code in an IIFE to keep the global scope clean:

(function () {
  "use strict";
  // your code
})();

Static pages, plainly

Most Code Pages are entirely static: you write exact HTML/CSS/JS, and exactly that renders. Nothing is substituted, queried, or computed on the way out.

The JS pane is not required for a page to work. Leave it empty and the page still renders, still styles correctly, still ships its content. JS is only for genuine client-side behavior: things that have to happen in the visitor's browser, after the page has loaded.

  • Toggling a mobile nav open and closed
  • Animating something on scroll
  • Validating a form before submit
  • Fetching something after the initial page load

Pulling in real WordPress content (a post's title, a list of recent posts) is never one of those things in Rawmark. That's the dynamic-content system below, and it happens entirely on the server, before your JS pane would even get a chance to run.

Dynamic, mechanism 1 of 3

Snippets

A Snippet is a reusable fragment of HTML/CSS/JS, stored once and referenced from any number of pages. Insert one with a self-closing marker:

Self-closing
<!-- rawmark:snippet id='42' -->

Snippets are live, not copied. The marker resolves to the snippet's current content on every render. Edit the snippet once, and every page referencing it updates immediately: there is no "paste and diverge" step. If you want a one-off variation, don't insert the snippet; copy its content into the page's own panes and edit that instead.

A snippet referencing a deleted or otherwise invalid ID resolves to nothing (see the fail-safe rule).

Dynamic, mechanism 3 of 3

Post Template

One Snippet, designated from the Snippets screen, becomes the layout for every ordinary WordPress Post that hasn't been individually flagged as its own Code Page. Write the layout once; seven merge tags substitute each Post's real data at render time:

TagResolves to
<!-- rawmark:post_title -->The post's title, escaped
<!-- rawmark:post_content -->Full content, through WordPress's real the_content filter chain (blocks, shortcodes, embeds all run)
<!-- rawmark:post_excerpt -->The excerpt, escaped
<!-- rawmark:post_date -->The formatted date, escaped
<!-- rawmark:featured_image -->Featured image markup, or nothing if none is set
<!-- rawmark:permalink -->The post's URL, escaped
<!-- rawmark:author_name -->The author's display name, escaped

An individually-flagged Post always renders its own source instead of the template. The specific case always wins over the general one.

Post Loop

Repeats a block of your own HTML once per matching Post: a filtered list, not a fixed layout. You write the markup; Rawmark supplies the data.

Paired
<!-- rawmark:post_loop category='news' tag='featured' count='5' -->
  <h2><!-- rawmark:post_title --></h2>
  <p><!-- rawmark:post_excerpt --></p>
  <a href="<!-- rawmark:permalink -->">Read more</a>
<!-- /rawmark:post_loop -->
AttributeDefaultNotes
categoryanyCategory slug. Omit to match every category.
taganyTag slug. Omit to match every tag.
count5Clamped to a maximum of 50 server-side, regardless of what's typed.

All seven Post Template tags work inside the loop body, resolving to that iteration's post. If nothing matches, the whole block resolves to nothing, same as an unset template.

Only works in a flagged Page or Post's own top-level source, not inside a Snippet inserted elsewhere, not in a header or footer.

Every post_loop needs its closing tag. Miss <!-- /rawmark:post_loop --> and the block you meant as a repeating template renders once, literally, as ordinary page content. Not a template, just text. The editor's lint indicator catches a mismatched open/close count; don't ignore it.

Loops don't nest. A post_loop placed inside another one closes the outer loop early. That's not supported, and it isn't silently fixed for you.

Shortcodes

Rawmark expands WordPress shortcodes — the [shortcode] bracket syntax — anywhere in a flagged Page or Post's rendered HTML, including content pulled in through a header, footer, or inserted Snippet. This runs through WordPress's own do_shortcode(), not the full the_content filter chain: shortcodes execute, but wpautop() and the other content filters never touch your markup.

This is what lets a plugin's own shortcode — WooCommerce's [woocommerce_cart], for example — render and stay interactive inside otherwise hand-authored HTML. See WooCommerce below for the specific recipe.

The live preview iframe does not expand shortcodes. It's built client-side from your draft HTML with no server round-trip, so a shortcode shows as literal bracket text while you're editing. It renders correctly once viewed on the real, published URL. Not a bug, just not wired up yet.

Any shortcode you reference runs whatever PHP that shortcode's plugin registered for it — the same trust boundary as the rest of a Rawmark pane. See Trust model.

Marker reference

Every marker lives inside an HTML comment on purpose, so it can never collide with a template-engine syntax like {{ }} that you might legitimately paste from somewhere else. Attributes are always single-quoted.

MarkerShapeWhere valid
Insert a Snippet<!-- rawmark:snippet id='N' -->Any page's HTML pane
Post data (7 tags)<!-- rawmark:post_title --> etc.Post Template source, or inside a post_loop body
Post Loop<!-- rawmark:post_loop ... --> ... <!-- /rawmark:post_loop -->Top-level flagged Page/Post source only
Integration

WooCommerce

Rawmark can own the markup and styling of a WooCommerce store's pages while WooCommerce keeps running everything dynamic underneath it: cart totals, coupon fields, payment gateway rendering, account forms. None of this touches WooCommerce's data model — Rawmark never reads or writes a product, order, or setting. It's markup layered on top of WooCommerce's own shortcode output, nothing more.

Cart, Checkout, My Account

These three work today with no extra step. WooCommerce already sets each one up with its page content being the relevant shortcode:

[woocommerce_cart]
[woocommerce_checkout]
[woocommerce_my_account]

Flag the page in Rawmark, keep that shortcode in the HTML pane, and wrap it in whatever markup and CSS the design calls for: a page-width container, a two-column layout, custom spacing. WooCommerce still renders the interactive form and totals wherever you place the shortcode, and its AJAX cart updates and localized script data (wc_cart_params and friends) keep working, because wp_head()/wp_footer() fire on a Rawmark page by default (see Not built yet — the toggle that could disable this has no editor UI, so it can't get switched off by accident).

Shop & Single Product

These two are different, and it's a WooCommerce constraint, not a Rawmark one: WooCommerce's own template loader (archive-product.php / single-product.php) runs for the Shop page and every product URL regardless of that page's content. Flagging WooCommerce's default Shop page in Rawmark has no effect; the theme's product templates still render underneath.

The working pattern is a fresh Page, not the default one:

  • Create a new Page, flag it in Rawmark, build the layout you want.
  • Drop in [products limit="12" columns="4"] for a shop-style grid, or [product_page id="107"] for a single product's full buy box.
  • Point navigation and any "Shop" links at this new page instead of WooCommerce's default one.

WooCommerce's product shortcodes take the same attributes documented in WooCommerce's own shortcode reference (category, orderby, columns, and so on). Rawmark doesn't add or restrict any of them; it only expands the shortcode, per Shortcodes above.

Don't try to restyle the locked Shop/Single Product templates directly. WooCommerce ignores their page content by design. Build a new Page with the shortcode approach above instead of fighting the template loader.

Integration

SureCart & blocks

SureCart's interactive pieces — Cart, Checkout, and every part of a product page except the title and description — are Gutenberg blocks, not shortcodes. Rawmark renders them, but only with Render blocks switched on and, for product pages, only inside the right wrapper. Both are covered below.

Turn on Render blocks

The Render blocks checkbox in the editor topbar (the enable_blocks setting) makes Rawmark run do_blocks() on the composed page before do_shortcode(), so real block markup renders and functions instead of sitting inert as HTML comments. It's off by default, because most Rawmark pages are hand-authored HTML with no block comments in them at all.

<!-- wp:surecart/checkout-form {"id":134} --><!-- /wp:surecart/checkout-form -->

For a Post Template, it's the Snippet's checkbox that matters — that's where the rendered source comes from.

Product pages: wrap in surecart/product-page

A product page's parts (price, media, quantity, buy buttons) are child blocks that declare surecart/product-page in their ancestor list. That parent is what supplies the product context, calls wp_interactivity_state(), emits the <form data-wp-interactive> element, and enqueues SureCart's @surecart/product-page script module. Rendered without it, the children emit structural markup with empty values and dead controls.

So wrap your own markup in the block. Everything inside it — your elements, your classes — passes through untouched:

<!-- wp:surecart/product-page -->
<main class="shop__product">
  <p class="shop__eyebrow">Shop</p>
  <!-- wp:surecart/product-title /-->
  <!-- wp:surecart/product-selected-price-amount /-->
  <!-- wp:surecart/product-quantity /-->
  <!-- wp:surecart/product-buy-buttons /-->
</main>
<!-- /wp:surecart/product-page -->

SureCart's product shortcodes are not a substitute for the wrapper. [sc_product_title] and [sc_product_description] work standalone because they read a PHP global. [sc_product_price], [sc_product_media], [sc_product_quantity] and [sc_product_cart_button] do not — they need the ancestor, and render empty and non-functional without it. That split is what makes this worth calling out: the page looks built because the title appears, while the price is blank and add-to-cart does nothing. [sc_product_list] and [sc_product_page id='…'] are self-contained and fine anywhere.

SureCart's cart itself is a slide-out drawer (surecart/slide-out-cart), not a routable page — there's no standalone "Cart" URL to flag the way Checkout has one.

Self-closing means self-closing

The block parser only recognizes a void block when the slash sits directly against the closer: /-->. Every near miss (<!-- wp:surecart/product-media / -->, <!-- wp:surecart/product-media/ -->) parses as plain text instead of a block. Nothing renders, nothing errors, and depending on how the comment got mangled the block's name can leak onto the page as visible text (wp:surecart/product-media /). If a block "doesn't render," compare its comment character for character with a known-good one before suspecting anything else.

Container blocks need their children

Some SureCart blocks are only wrappers. surecart/product-buy-buttons renders a bare div around whatever is inside it, and the actual buttons are separate surecart/product-buy-button blocks you nest yourself. A self-closing <!-- wp:surecart/product-buy-buttons /--> renders an empty container, which reads as "the buy button is missing." The same nesting rule applies to product-collection-tags / product-collection-tag, product-variant-pills / product-variant-pill, and product-price-chooser / product-price-choice-template. SureCart's own single-product template (in the plugin's templates/ directory) is the reference for what nests inside what.

<!-- wp:surecart/product-buy-buttons -->
<div class="wp-block-surecart-product-buy-buttons wp-block-buttons is-layout-flex">
  <!-- wp:surecart/product-buy-button {"add_to_cart":true,"text":"Add To Cart"} /-->
  <!-- wp:surecart/product-buy-button {"text":"Buy Now","className":"is-style-outline"} /-->
</div>
<!-- /wp:surecart/product-buy-buttons -->

One SureCart setting does affect what you get: if the cart is disabled in SureCart, every buy button silently becomes a Buy Now (redirect to checkout) regardless of its add_to_cart attribute. It never hides the button outright.

Preview as: pick the SureCart product

A Snippet has no post of its own, so the editor's Preview as picker renders the template against a real post or product. The list groups candidates by post type. If the store runs WooCommerce and SureCart side by side and both have a product with the same name, picking the WooCommerce one renders every SureCart block empty (its query finds no sc_product with that ID). The fix is reading the optgroup label, not the item title. Same rule for any template: the preview product must match the blocks in the markup.

The preview iframe is sandboxed without allow-same-origin, on purpose. SureCart's interactive pieces load as ES modules and call the REST API, both of which an opaque-origin iframe cannot do. So the preview is an accurate check of layout, images, price, and styles, while add-to-cart clicks, variant switching, and the quantity stepper only work on the live URL. Judge looks in the preview, behavior on the real page.

Some blocks wait for data

Variant pills, the price chooser, the ad-hoc amount field, trial and fee lines, and the price interval all self-hide unless the product actually has that data (server-side hidden, no empty chrome). The review summary shows zeros until the first review, and Related Products stays empty while the store has a single product. An empty spot where one of these should be usually means "no data yet," not a broken template.

Contain the media slider

The media block renders as .sc-image-media (the outer slider) wrapping slides that each hold a figure.wp-block-surecart-product-media. Cap the outer element, and keep the grid tracks it lives in from stretching to fit the slider's intrinsic width (minmax(0, 1fr), not bare 1fr). Without both, the slide expands thousands of pixels past the viewport and the centered image scrolls off screen, leaving a page-width blank box with a slider arrow in it.

.product__container { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.product .sc-image-media { max-width: 520px; margin: 0 auto; }
.product .sc-image-media img { max-height: 560px; object-fit: contain; }

A complete product page, working example

This is a full product detail template running live against SureCart: media gallery, collection tags, review summary, title, price row with scratch amount and sale badge, trial and fee lines, description, variant pills, price chooser, quantity, ad-hoc amount, Add to Cart and Buy Now, then Customer Reviews and Related Products below the form. Blocks that have no data on a given product simply hide themselves.

HTML pane (Render blocks on):

<!-- wp:surecart/product-page -->
<main class="rawmark-product">

  <div class="rawmark-product__container">

    <!-- LEFT: PRODUCT MEDIA -->
    <section class="rawmark-product__media">
      <!-- wp:surecart/product-media /-->
    </section>

    <!-- RIGHT: PRODUCT INFO -->
    <section class="rawmark-product__info">

      <!-- COLLECTION TAGS -->
      <div class="rawmark-product__collections">
        <!-- wp:surecart/product-collection-tags -->
        <!-- wp:surecart/product-collection-tag /-->
        <!-- /wp:surecart/product-collection-tags -->
      </div>

      <!-- REVIEW SUMMARY (stars + count) -->
      <div class="rawmark-product__rating">
        <!-- wp:surecart/product-review-average-rating-stars /-->
        <!-- wp:surecart/product-review-total-rating /-->
      </div>

      <!-- PRODUCT TITLE -->
      <div class="rawmark-product__title">
        <!-- wp:surecart/product-title /-->
      </div>

      <!-- PRICE ROW: scratch + amount + interval + sale badge -->
      <div class="rawmark-product__price">
        <!-- wp:surecart/product-selected-price-scratch-amount /-->
        <!-- wp:surecart/product-selected-price-amount /-->
        <!-- wp:surecart/product-selected-price-interval /-->
        <!-- wp:surecart/product-sale-badge /-->
      </div>

      <!-- TRIAL + SETUP FEE (self-hide when the price has neither) -->
      <div class="rawmark-product__price-meta">
        <!-- wp:surecart/product-selected-price-trial /-->
        <!-- wp:surecart/product-selected-price-fees /-->
      </div>

      <!-- PRODUCT DESCRIPTION -->
      <div class="rawmark-product__description">
        <!-- wp:surecart/product-description /-->
      </div>

      <!-- VARIANT PILLS (self-hide when the product has no variants) -->
      <div class="rawmark-product__variants">
        <!-- wp:surecart/product-variant-pills -->
        <!-- wp:surecart/product-variant-pill /-->
        <!-- /wp:surecart/product-variant-pills -->
      </div>

      <!-- PRICE CHOOSER (self-hides when the product has a single price) -->
      <div class="rawmark-product__price-choices">
        <!-- wp:surecart/product-price-chooser -->
        <!-- wp:surecart/product-price-choice-template {"layout":{"type":"flex","justifyContent":"space-between","flexWrap":"nowrap","orientation":"horizontal"}} -->
        <!-- wp:surecart/price-name /-->
        <!-- wp:surecart/price-scratch-amount /-->
        <!-- wp:surecart/price-amount /-->
        <!-- wp:surecart/price-interval /-->
        <!-- wp:surecart/price-trial /-->
        <!-- wp:surecart/price-setup-fee /-->
        <!-- /wp:surecart/product-price-choice-template -->
        <!-- /wp:surecart/product-price-chooser -->
      </div>

      <!-- QUANTITY -->
      <div class="rawmark-product__quantity">
        <!-- wp:surecart/product-quantity /-->
      </div>

      <!-- PAY-WHAT-YOU-WANT (self-hides unless the price is ad-hoc) -->
      <div class="rawmark-product__adhoc">
        <!-- wp:surecart/product-selected-price-ad-hoc-amount /-->
      </div>

      <!-- PURCHASE ACTIONS -->
      <div class="rawmark-product__actions">
        <!-- wp:surecart/product-buy-buttons {"style":{"spacing":{"blockGap":"5px"}}} -->
        <div class="wp-block-surecart-product-buy-buttons wp-block-buttons sc-block-buttons is-layout-flex">
          <!-- wp:surecart/product-buy-button {"add_to_cart":true,"text":"Add To Cart"} /-->
          <!-- wp:surecart/product-buy-button {"text":"Buy Now","className":"is-style-outline"} /-->
        </div>
        <!-- /wp:surecart/product-buy-buttons -->
      </div>

    </section>
<!-- /wp:surecart/product-page -->

<!-- CUSTOMER REVIEWS -->
<section class="rawmark-reviews">
  <div class="rawmark-reviews__head">
    <h2 class="rawmark-reviews__heading">Customer Reviews</h2>
  </div>

  <!-- wp:surecart/product-review-list {"query":{"perPage":10,"pages":0,"offset":0}} -->
  <!-- wp:surecart/product-reviews -->

  <!-- wp:surecart/product-review-summary -->
  <div class="rawmark-reviews__summary">
    <div class="rawmark-reviews__average">
      <!-- wp:surecart/product-review-average-rating-value /-->
      <!-- wp:surecart/product-review-average-rating-stars /-->
      <!-- wp:surecart/product-review-total-rating /-->
    </div>
    <div class="rawmark-reviews__breakdown">
      <!-- wp:surecart/product-review-breakdown {"columns":2} /-->
    </div>
  </div>
  <!-- /wp:surecart/product-review-summary -->

  <div class="rawmark-reviews__actions">
    <!-- wp:surecart/product-review-add-button {"width":100} /-->
  </div>

  <!-- wp:surecart/product-review-template {"layout":{"type":"grid","columnCount":1}} -->
  <article class="rawmark-review">
    <header class="rawmark-review__head">
      <span class="rawmark-review__who">
        <!-- wp:surecart/product-review-reviewer-name /-->
        <!-- wp:surecart/product-review-verified-badge {"label":"Verified Buyer"} /-->
      </span>
      <!-- wp:surecart/product-review-date {"format":"human-diff"} /-->
    </header>
    <!-- wp:surecart/product-review-rating-stars /-->
    <!-- wp:surecart/product-review-title /-->
    <!-- wp:surecart/product-review-content /-->
  </article>
  <!-- /wp:surecart/product-review-template -->

  <!-- /wp:surecart/product-reviews -->
  <!-- /wp:surecart/product-review-list -->
</section>

<!-- RELATED PRODUCTS -->
<section class="rawmark-related">
  <h2 class="rawmark-related__heading">You may also like</h2>

  <!-- wp:surecart/product-list-related -->
  <!-- wp:surecart/product-template -->
  <div class="rawmark-card">
    <!-- wp:cover {"useFeaturedImage":true,"dimRatio":0,"isUserOverlayColor":true,"style":{"dimensions":{"aspectRatio":"1"}}} -->
    <div class="wp-block-cover"><span aria-hidden="true" class="wp-block-cover__background has-background-dim-0 has-background-dim"></span><div class="wp-block-cover__inner-container"></div></div>
    <!-- /wp:cover -->
    <div class="rawmark-card__body">
      <!-- wp:surecart/product-title {"level":3} /-->
      <div class="rawmark-card__price">
        <!-- wp:surecart/product-list-price /-->
        <!-- wp:surecart/product-scratch-price /-->
      </div>
    </div>
  </div>
  <!-- /wp:surecart/product-template -->
  <!-- /wp:surecart/product-list-related -->
</section>


  </div>

</main>
<!-- /wp:surecart/product-page -->

CSS pane:

/* ==========================================================================
   Product detail — final pass against the confirmed-working HTML
   structure and SureCart's official developer docs.

   Key finding that resolved a lot of earlier hedging: SureCart's own
   docs state plainly that v3 dropped Shadow DOM everywhere except
   Checkout Forms/Pages. The product page, quantity selector, and buy
   buttons here are plain light-DOM HTML - no ::part() needed, normal
   CSS selectors work directly. (Checkout, separately, still needs the
   Shadow DOM / CSS-custom-property approach - different page, not
   touched here.)

   Quantity selector structure independently confirmed against
   developer.surecart.com/docs/quantity-selector's own source: parent
   must carry class sc-quantity-selector, matches what's used below.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap');

:root {
  --paper: #F0EFE9;
  --surface: #FFFFFF;
  --ink: #201E1B;
  --ink-soft: #4A463F;
  --muted: #8D877C;
  --line: #DEDACF;
  --pine: #2E5C46;
  --pine-deep: #1F4432;
  --font-display: 'Fraunces', Georgia, serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, monospace;
  --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 24px;
  --space-6: 32px; --space-7: 48px; --space-8: 64px;
  --radius: 6px;
  --max-width: 1180px;
}

* { box-sizing: border-box; }
body { background: var(--paper); color: var(--ink); font-family: var(--font-body); }

/* ---- Layout: outer main + the real two-column container ---- */

.rawmark-product {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--space-7) var(--space-5) var(--space-8);
}

.rawmark-product__container {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-7);
  align-items: start;
}
@media (max-width: 780px) { .rawmark-product__container { grid-template-columns: 1fr; } }

.rawmark-product__media { grid-column: 1; }
.rawmark-product__info { grid-column: 2; }
@media (max-width: 780px) { .rawmark-product__media, .rawmark-product__info { grid-column: 1; } }

/* ---- Media ---- */

.rawmark-product .sc-image-media {
  max-width: 520px;
  margin: 0 auto;
}
@media (max-width: 780px) {
  .rawmark-product .sc-image-media { max-width: 100%; }
}
.rawmark-product .wp-block-surecart-product-media {
  position: relative;
  width: 100%;
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.rawmark-product .wp-block-surecart-product-media figure { margin: 0; }
.rawmark-product .wp-block-surecart-product-media img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 560px;
  object-fit: contain;
}
@media (max-width: 780px) {
  .rawmark-product .wp-block-surecart-product-media { max-width: 100%; }
  .rawmark-product .wp-block-surecart-product-media img { max-height: 400px; }
}
.rawmark-product .lightbox-trigger {
  position: absolute;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255,255,255,0.9);
  border: none;
  border-radius: 50%;
  cursor: pointer;
}

/* ---- Heading: title + price ---- */

.rawmark-product__heading { margin-bottom: var(--space-4); }

.rawmark-product .wp-block-surecart-product-title {
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 var(--space-2);
}

.rawmark-product__price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--line);
}
.rawmark-product .wp-block-surecart-product-selected-price-amount {
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 600;
  color: var(--pine);
}
.rawmark-product .wp-block-surecart-product-selected-price-scratch-amount {
  font-family: var(--font-mono);
  font-size: 16px;
  color: var(--muted);
  text-decoration: line-through;
  margin-right: var(--space-2);
}
.rawmark-product .wp-block-surecart-product-sale-badge:not([hidden]) {
  display: inline-block;
  width: fit-content;
  font-family: var(--font-mono);
  font-size: 10px !important;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: var(--pine) !important;
  color: var(--surface) !important;
  padding: 3px 8px !important;
  border-radius: 3px !important;
  margin-left: var(--space-2);
}

/* ==========================================================================
   Extended product page: collections, rating, price meta, variants,
   price chooser, ad-hoc, reviews, related products.
   ========================================================================== */

/* ---- Collection tags ---- */
.rawmark-product .wp-block-surecart-product-collection-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--space-3); }
.rawmark-product .wp-block-surecart-product-collection-tag,
.rawmark-product .wp-block-surecart-product-collection-tag a {
  font-family: var(--font-mono); font-size: 10px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--pine); text-decoration: none;
  border: 1px solid var(--pine); border-radius: 999px; padding: 3px 10px;
}
.rawmark-product .wp-block-surecart-product-collection-tag a:hover { background: var(--pine); color: var(--surface); }

/* ---- Rating summary row ---- */
.rawmark-product__rating { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-2); font-size: 13px; color: var(--muted); }
.rawmark-product__rating svg { width: 14px; height: 14px; }

/* ---- Interval text in the price row ---- */
.rawmark-product .wp-block-surecart-product-selected-price-interval { font-family: var(--font-mono); font-size: 13px; color: var(--muted); }

/* ---- Price meta: trial + setup fee ---- */
.rawmark-product__price-meta { display: flex; gap: var(--space-3); margin: var(--space-2) 0 0; font-size: 12px; color: var(--muted); }

/* ---- Variant pills ---- */
.rawmark-product__variants { margin: 0 0 var(--space-5); }
.rawmark-product__variants fieldset { border: 0; margin: 0 0 var(--space-3); padding: 0; }
.rawmark-product__variants legend,
.rawmark-product__variants .sc-form-label { display: block; font-size: 13px; font-weight: 500; color: var(--ink-soft); margin: 0 0 var(--space-2); padding: 0; }
.rawmark-product__variants button {
  font-family: var(--font-body); font-size: 13px; font-weight: 500;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 6px 14px; margin: 0 6px 6px 0; cursor: pointer;
}
.rawmark-product__variants button:hover { border-color: var(--pine); color: var(--pine); }
.rawmark-product__variants button[aria-checked="true"],
.rawmark-product__variants button.is-selected,
.rawmark-product__variants button.sc-selected { border-color: var(--pine); color: var(--pine); box-shadow: inset 0 0 0 1px var(--pine); }

/* ---- Price chooser rows ---- */
.rawmark-product__price-choices { margin: 0 0 var(--space-5); }
.rawmark-product .wp-block-surecart-product-price-choice-template {
  display: flex; align-items: baseline; gap: var(--space-2); width: 100%;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-2); cursor: pointer;
}
.rawmark-product .wp-block-surecart-product-price-choice-template:hover { border-color: var(--pine); }
.rawmark-product .wp-block-surecart-product-price-choice-template:has(:checked) { border-color: var(--pine); box-shadow: inset 0 0 0 1px var(--pine); }
.rawmark-product .wp-block-surecart-price-name { font-weight: 600; font-size: 14px; }
.rawmark-product .wp-block-surecart-price-amount,
.rawmark-product .wp-block-surecart-price-scratch-amount { font-family: var(--font-mono); }
.rawmark-product .wp-block-surecart-price-scratch-amount { color: var(--muted); text-decoration: line-through; font-size: 12px; }

/* ---- Ad-hoc (pay what you want) ---- */
.rawmark-product__adhoc { margin: 0 0 var(--space-5); }
.rawmark-product__adhoc input {
  width: 140px; font-family: var(--font-mono); font-size: 14px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
}

/* ---- Reviews ---- */
.rawmark-reviews { max-width: var(--max-width); margin: 0 auto; padding: 0 var(--space-5) var(--space-8); }
.rawmark-reviews__heading { font-family: var(--font-display); font-size: 26px; font-weight: 600; color: var(--ink); margin: 0 0 var(--space-5); }
.rawmark-reviews__summary {
  display: flex; flex-wrap: wrap; gap: var(--space-6); align-items: center;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: var(--space-5); margin: 0 0 var(--space-5);
}
.rawmark-reviews__average { display: flex; align-items: center; gap: var(--space-3); }
.rawmark-reviews__average .wp-block-surecart-product-review-average-rating-value { font-family: var(--font-display); font-size: 32px; font-weight: 600; color: var(--ink); }
.rawmark-reviews__breakdown { flex: 1; min-width: 240px; }
.rawmark-reviews__actions { margin: 0 0 var(--space-5); }
.rawmark-review { border-bottom: 1px solid var(--line); padding: var(--space-5) 0; }
.rawmark-review__head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); margin: 0 0 var(--space-2); font-size: 13px; color: var(--muted); }
.rawmark-review__who { display: inline-flex; align-items: center; gap: var(--space-2); font-weight: 500; color: var(--ink); }
.rawmark-review .wp-block-surecart-product-review-title { font-size: 16px; font-weight: 700; color: var(--ink); margin: var(--space-2) 0; }
.rawmark-review .wp-block-surecart-product-review-content { font-size: 14px; line-height: 1.6; color: var(--ink-soft); }

/* ---- Related products ---- */
.rawmark-related { max-width: var(--max-width); margin: 0 auto; padding: 0 var(--space-5) var(--space-8); }
.rawmark-related__heading { font-family: var(--font-display); font-size: 26px; font-weight: 600; color: var(--ink); margin: 0 0 var(--space-5); }
.rawmark-related .wp-block-surecart-product-template { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--space-5); }
.rawmark-related .wp-block-surecart-product-template > li { list-style: none; }
.rawmark-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.rawmark-card .wp-block-cover { margin: 0; min-height: 0; }
.rawmark-card__body { padding: var(--space-4); }
.rawmark-card__body .wp-block-surecart-product-title { font-family: var(--font-display); font-size: 16px; font-weight: 600; color: var(--ink); margin: 0 0 var(--space-2); }
.rawmark-card__price { display: flex; align-items: baseline; gap: var(--space-2); font-family: var(--font-mono); font-size: 14px; color: var(--ink); }
.rawmark-card__price .wp-block-surecart-product-scratch-price { color: var(--muted); text-decoration: line-through; font-size: 12px; }

Naming & formatting

Not enforced by the editor. This is what keeps a page readable when you return to it later.

  • Class names: kebab-case. A block__element--modifier pattern survives being read out of context.
  • Prefix per page: a short prefix per page (lp__, pricing__). Costs nothing, and matters the moment content moves between pages.
  • Never use the rawmark- prefix for your own classes. It's reserved for classes Rawmark itself adds (like the rawmark-page body class).
  • Indentation: two spaces, in all three panes.
  • Attributes: lowercase, double-quoted.
  • Section banners: a comment above each major block. In a single-file page with no folder structure, comments are the only navigation you get.

Accessibility baseline

A visual builder handles some of this automatically. Rawmark handles none of it, so treat this as the minimum bar:

  • Exactly one <h1>, heading levels that descend without skipping
  • Landmarks: <main>, and <nav>/<header>/<footer> where they apply
  • Every image has alt (empty alt="" for decorative images, never a missing attribute)
  • Every form input has a real <label>, not a placeholder standing in for one
  • Visible focus states: replace outline: none, don't just remove it
  • Text contrast at 4.5:1 or better
  • Interactive elements are <button> or <a>, never a <div> with a click handler

The fail-safe rule

One rule, applied identically everywhere in the system: a broken or missing reference resolves to nothing. Never a fatal error, never a warning banner on the live page.

A Snippet marker pointing at a deleted Snippet, a Header/Footer Template pointing at a deleted Snippet, a Post Template that's never been set, a Post Loop whose category slug has a typo and matches nothing: all of these render as empty, exactly as if that content were never there. Write your templates and loops knowing that an empty result is a valid, unremarkable outcome, not a signal that something crashed.

One exception: this silence applies to references, not to malformed markup. An unclosed post_loop doesn't fail safe. See the callout in Post Loop above.

Trust model

Writing to the HTML/CSS/JS panes requires the rawmark_edit_code capability, granted to Administrators only by default. Pane content is not sanitized on output, by design: the same trust level as editing a theme's PHP files directly, not the trust level of a comment field. Anyone holding that capability can write arbitrary JavaScript that runs for every visitor, or reference any registered shortcode, which runs whatever PHP that shortcode's plugin defined.

Grant it accordingly. It is not a role for a general content editor.

Size limits

Soft warning

256 KB combined across all three panes. The editor flags it, but saving still works.

Hard cap, per pane

1 MB. A save past this is rejected.

Hard cap, combined

2 MB across HTML + CSS + JS together.

Post Loop count

Clamped to 50 matching posts, regardless of what's requested.

Editor conventions

The three-pane editor adapts to what you're editing:

  • A Page or Post: title is editable, the status pill shows Draft/Published, and there are two save actions: Save draft and Publish/Update. "Save as Snippet," the header/footer selects, and "Insert Snippet" all appear only here.
  • A Snippet: title is read-only (renaming isn't supported yet), no status pill, and a single Save button. Snippets have no draft/publish state to choose between.

"Insert Media" (the image icon in the toolbar) works in both contexts and inserts at the cursor in whichever pane is active. The Render blocks checkbox also appears in both, and saves on its own the moment you toggle it — no separate Save needed. A Snippet additionally gets a Preview as picker, since it has no post of its own to render against; choosing a real post or product there is what makes merge tags and product blocks resolve in the preview pane.

Not built yet

Stated plainly, so nothing here gets assumed into existence:

  • No placeholder tokens. A Snippet has no {{variable}} substitution system. Its content is exactly what's stored, resolved live per the rule above.
  • No shared/global stylesheet feature. Nothing writes a site-wide CSS file for you; link your own if you want one shared across pages.
  • No MCP or AI-agent abilities are registered. Nothing in the plugin currently exposes page/snippet operations to an external agent.
  • SEO title, SEO description, and the wp_head/wp_footer toggles exist in the data model but have no editor UI and are not writable over the REST API. In practice every page uses the real post/page title, no meta description, and both hooks always fire. (enable_blocks was in this list until it got the Render blocks checkbox — see SureCart & blocks.)