{
  "@context": {
    "@vocab": "https://schema.org/",
    "vibed": "https://vibed.thecardproject.uk/vocab#",
    "contractVersion": "vibed:contractVersion",
    "manifestSchema": {
      "@id": "vibed:manifestSchema",
      "@type": "@json"
    },
    "designSchema": {
      "@id": "vibed:designSchema",
      "@type": "@json"
    },
    "svgVocabulary": {
      "@id": "vibed:svgVocabulary",
      "@type": "@json"
    },
    "exampleSvg": "vibed:exampleSvg",
    "specificationUrl": {
      "@id": "vibed:specificationUrl",
      "@type": "@id"
    },
    "specificationVersion": "vibed:specificationVersion"
  },
  "@type": "HowTo",
  "@id": "https://vibed.thecardproject.uk/#design-contract",
  "name": "Create an editable PersoFiles card for Vibed",
  "description": "Fetch and read https://perso.thecardproject.uk/perso-schema in full before generating a design. That is the authoritative PersoFile specification; apply its cardProfile. Embedded schemas, rules and example are a convenience mirror of pinned specification 1.0.0, not an independent format. Preview the actual SVG when supported, iterate with the customer, then return one complete copyable SVG code block; a downloadable file is optional.",
  "url": "https://vibed.thecardproject.uk/",
  "contractVersion": "1.2.0",
  "specificationUrl": "https://perso.thecardproject.uk/perso-schema",
  "specificationVersion": "1.0.0",
  "subjectOf": {
    "@type": "TechArticle",
    "url": "https://perso.thecardproject.uk/perso-schema",
    "version": "1.0.0",
    "name": "PersoFile authoring specification"
  },
  "encodingFormat": "image/svg+xml",
  "step": [
    {
      "@type": "HowToStep",
      "position": 1,
      "name": "Deliverable",
      "text": "Work with the customer iteratively. If your tools can render SVG, first show a rendered preview of the actual SVG for every face, invite changes and regenerate the preview after edits. Never substitute an AI-generated raster approximation for the SVG. If rendering is unavailable, say so and offer Vibed as the preview. When the customer is ready, deliver the complete final SVG in ONE copyable fenced code block labelled svg, with no prose inside the code block. Also offer a downloadable UTF-8 .svg file if your tools support it; a file is optional. Never split artwork or metadata across multiple code blocks or return an HTML or separate JSON attachment. Do not call any Vibed API or web agent: Vibed publishes this contract only. Follow the customer brief while obeying this output contract."
    },
    {
      "@type": "HowToStep",
      "position": 2,
      "name": "Document",
      "text": "The SVG must be well-formed XML with xmlns=\"http://www.w3.org/2000/svg\" and root viewBox=\"0 0 W H\" with positive numeric W/H. Maximum 2,000,000 UTF-8 bytes and 20,000 nodes. Give all artwork elements globally unique IDs."
    },
    {
      "@type": "HowToStep",
      "position": 3,
      "name": "Surfaces",
      "text": "Root direct children may only be title, desc, defs, and named nested svg surfaces. A single-sided card has exactly one data-surface=\"front\". A duplex card has exactly front and rear. Never use back, Front, Rear, numbers, duplicate names or nested surface declarations. Display labels may be Front and Rear."
    },
    {
      "@type": "HowToStep",
      "position": 4,
      "name": "Dimensions",
      "text": "Each surface is a direct child svg with data-surface, data-label, data-width-mm, data-height-mm, viewBox=\"0 0 W H\", x, y, width, height. Physical dimensions are millimetres, 1 to 10000. Use 85.6 by 54 mm unless asked otherwise. At 10 SVG units/mm use viewBox=\"0 0 856 540\". Surface viewBox ratio must match widthMm/heightMm within 0.001. Lay out front/rear side by side without overlap and fit the root viewBox around them."
    },
    {
      "@type": "HowToStep",
      "position": 5,
      "name": "Embedded manifest",
      "text": "Include exactly one root <desc id=\"vibed-design\"> whose text is JSON conforming to manifestSchema. XML-escape JSON text: & becomes &amp; and < becomes &lt;. Do not use metadata, script or custom XML elements. schemaVersion is 2. Manifest has name, description, tags and fields. No svg property inside the manifest (avoid recursion)."
    },
    {
      "@type": "HowToStep",
      "position": 6,
      "name": "Fields",
      "text": "For each editable value define {key,label,type,required,defaultValue} in fields, up to 60. Keys are unique, match ^[a-zA-Z][a-zA-Z0-9_]{0,63}$, and bind only via data-field=\"key\" on text, tspan or image. Every binding must be declared and every declared field must appear in artwork. Share keys across faces to personalize together. Do not bind g, rect, path, or static labels. Avoid nesting one bound text field inside another."
    },
    {
      "@type": "HowToStep",
      "position": 7,
      "name": "Text",
      "text": "Text fields bind to text/tspan with sample text matching defaultValue; keep labels separate. Rendering replaces the bound element textContent, not markup. Explicit x/y, font-family=\"Arial\", font-size, fill and text-anchor. No automatic wrapping: use separate positioned text/tspan elements, including repeated bindings if intentional. Leave enough room for longer values."
    },
    {
      "@type": "HowToStep",
      "position": 8,
      "name": "Images",
      "text": "Image fields bind to an image element with explicit x/y/width/height and preserveAspectRatio=\"xMidYMid slice\" for portraits or \"xMidYMid meet\" for logos. Image field defaultValue is empty string. Leave href absent for an empty placeholder; draw a static background rect beneath it. If a real initial image is supplied, embed PNG/JPEG/WebP as base64 in href. Never bind a silhouette/group as a photo. Preserve logos, do not invent them."
    },
    {
      "@type": "HowToStep",
      "position": 9,
      "name": "SVG vocabulary",
      "text": "Use editable vectors first. Allowed elements and attributes are listed in svgVocabulary. Presentation attributes only; no style attributes, style elements, CSS, scripts, event handlers, foreignObject, use, filter, animation, DOCTYPE, entities or processing instructions. No external fonts, URLs or network resources. Paint references must be local url(#id). href/xlink:href must be a local #id or embedded PNG/JPEG/WebP data URL. Gradients/clipPaths may live in shared root defs; IDs unique across both faces."
    },
    {
      "@type": "HowToStep",
      "position": 10,
      "name": "Print design",
      "text": "Design for physical readability with at least 3 mm safe margins. Prefer restrained colors, clear hierarchy and fictional sample personal details unless provided by the customer. Do not invent official accreditation, machine-readable credentials or scannable codes. Keep all artwork within its surface."
    },
    {
      "@type": "HowToStep",
      "position": 11,
      "name": "Validation",
      "text": "Check XML, manifest JSON Schema, field bindings, unique IDs, exact surface names, dimensions, local references and allowed SVG vocabulary before delivery. The complete worked example is valid and can be adapted. JSON Schema alone does not validate SVG semantics."
    },
    {
      "@type": "HowToStep",
      "position": 12,
      "name": "Saving",
      "text": "Vibed extracts the manifest, adds svg as a string and validates it as designSchema (the PersoFiles v2 saveable design). Preview/personalization run locally in the browser. Users can download personalized SVG or canonical design JSON for later storage/printing; this prototype does not persist to a server or print."
    }
  ],
  "manifestSchema": {
    "$schema": "https://json-schema.org/draft-07/schema#",
    "$id": "https://perso.thecardproject.uk/perso-schema/1.0.0.json#manifestSchema",
    "title": "Vibed SVG manifest / PersoFiles v2 design metadata",
    "type": "object",
    "additionalProperties": false,
    "required": [
      "schemaVersion",
      "name",
      "description",
      "tags",
      "fields"
    ],
    "properties": {
      "schemaVersion": {
        "const": 2
      },
      "name": {
        "type": "string",
        "minLength": 1,
        "maxLength": 100
      },
      "description": {
        "type": "string",
        "maxLength": 2000
      },
      "tags": {
        "type": "array",
        "maxItems": 30,
        "uniqueItems": true,
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 50
        }
      },
      "fields": {
        "type": "array",
        "maxItems": 60,
        "items": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "key",
            "label",
            "type",
            "required",
            "defaultValue"
          ],
          "properties": {
            "key": {
              "type": "string",
              "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,63}$"
            },
            "label": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "type": {
              "enum": [
                "text",
                "image"
              ]
            },
            "required": {
              "type": "boolean"
            },
            "defaultValue": {
              "type": "string",
              "maxLength": 2000,
              "description": "Text sample/default. For images use empty string; optional initial image belongs in SVG href."
            }
          }
        }
      }
    }
  },
  "designSchema": {
    "$schema": "https://json-schema.org/draft-07/schema#",
    "$id": "https://perso.thecardproject.uk/perso-schema/1.0.0.json#designSchema",
    "title": "PersoFiles v2 saveable design",
    "type": "object",
    "additionalProperties": false,
    "required": [
      "schemaVersion",
      "name",
      "description",
      "tags",
      "fields",
      "svg"
    ],
    "properties": {
      "schemaVersion": {
        "const": 2
      },
      "name": {
        "type": "string",
        "minLength": 1,
        "maxLength": 100
      },
      "description": {
        "type": "string",
        "maxLength": 2000
      },
      "tags": {
        "type": "array",
        "maxItems": 30,
        "uniqueItems": true,
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 50
        }
      },
      "fields": {
        "type": "array",
        "maxItems": 60,
        "items": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "key",
            "label",
            "type",
            "required",
            "defaultValue"
          ],
          "properties": {
            "key": {
              "type": "string",
              "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,63}$"
            },
            "label": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "type": {
              "enum": [
                "text",
                "image"
              ]
            },
            "required": {
              "type": "boolean"
            },
            "defaultValue": {
              "type": "string",
              "maxLength": 2000,
              "description": "Text sample/default. For images use empty string; optional initial image belongs in SVG href."
            }
          }
        }
      },
      "svg": {
        "type": "string",
        "maxLength": 2000000,
        "description": "Complete standalone SVG. Semantic SVG rules in the home page JSON-LD are mandatory in addition to this JSON Schema."
      }
    }
  },
  "svgVocabulary": {
    "elements": [
      "svg",
      "g",
      "defs",
      "rect",
      "circle",
      "ellipse",
      "line",
      "polyline",
      "polygon",
      "path",
      "text",
      "tspan",
      "linearGradient",
      "radialGradient",
      "stop",
      "clipPath",
      "mask",
      "pattern",
      "image",
      "title",
      "desc"
    ],
    "attributes": [
      "xmlns",
      "xmlns:xlink",
      "viewBox",
      "width",
      "height",
      "x",
      "y",
      "x1",
      "x2",
      "y1",
      "y2",
      "cx",
      "cy",
      "r",
      "rx",
      "ry",
      "d",
      "points",
      "fill",
      "fill-opacity",
      "fill-rule",
      "stroke",
      "stroke-width",
      "stroke-opacity",
      "stroke-linecap",
      "stroke-linejoin",
      "stroke-dasharray",
      "opacity",
      "transform",
      "id",
      "clip-path",
      "clip-rule",
      "clipPathUnits",
      "mask",
      "maskUnits",
      "maskContentUnits",
      "font-family",
      "font-size",
      "font-weight",
      "font-style",
      "text-anchor",
      "dominant-baseline",
      "letter-spacing",
      "textLength",
      "lengthAdjust",
      "dx",
      "dy",
      "gradientUnits",
      "gradientTransform",
      "offset",
      "stop-color",
      "stop-opacity",
      "spreadMethod",
      "patternUnits",
      "patternTransform",
      "preserveAspectRatio",
      "href",
      "xlink:href",
      "data-children-locked",
      "data-asset-id",
      "data-asset-revision",
      "data-field",
      "data-surface",
      "data-label",
      "data-width-mm",
      "data-height-mm"
    ]
  },
  "exampleSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1728 540\">\n  <title>Studio membership</title>\n  <desc id=\"vibed-design\">{\"schemaVersion\":2,\"name\":\"Studio membership\",\"description\":\"A two-sided membership card\",\"tags\":[\"membership\"],\"fields\":[{\"key\":\"holderName\",\"label\":\"Your name\",\"type\":\"text\",\"required\":true,\"defaultValue\":\"Alex Morgan\"},{\"key\":\"role\",\"label\":\"Role\",\"type\":\"text\",\"required\":false,\"defaultValue\":\"Creative member\"},{\"key\":\"photo\",\"label\":\"Portrait\",\"type\":\"image\",\"required\":false,\"defaultValue\":\"\"}]}</desc>\n  <svg id=\"front-face\" data-surface=\"front\" data-label=\"Front\" data-width-mm=\"85.6\" data-height-mm=\"54\" viewBox=\"0 0 856 540\" x=\"0\" y=\"0\" width=\"856\" height=\"540\">\n    <rect id=\"front-bg\" width=\"856\" height=\"540\" rx=\"24\" fill=\"#143e39\"/>\n    <circle id=\"front-orbit\" cx=\"800\" cy=\"40\" r=\"240\" fill=\"#22594f\"/>\n    <text id=\"brand\" x=\"48\" y=\"80\" fill=\"#d2efb0\" font-family=\"Arial\" font-size=\"24\" letter-spacing=\"4\">STUDIO / MEMBERSHIP</text>\n    <rect id=\"photo-bg\" x=\"48\" y=\"135\" width=\"184\" height=\"236\" rx=\"12\" fill=\"#3b6c61\"/>\n    <image id=\"portrait\" x=\"48\" y=\"135\" width=\"184\" height=\"236\" data-field=\"photo\" preserveAspectRatio=\"xMidYMid slice\"/>\n    <text id=\"holder-label\" x=\"274\" y=\"182\" fill=\"#a7c5b7\" font-family=\"Arial\" font-size=\"18\">CARDHOLDER</text>\n    <text id=\"holder\" x=\"274\" y=\"235\" fill=\"#ffffff\" font-family=\"Arial\" font-size=\"38\" font-weight=\"700\" data-field=\"holderName\">Alex Morgan</text>\n    <text id=\"role\" x=\"274\" y=\"280\" fill=\"#d2efb0\" font-family=\"Arial\" font-size=\"24\" data-field=\"role\">Creative member</text>\n    <line id=\"rule\" x1=\"48\" y1=\"430\" x2=\"808\" y2=\"430\" stroke=\"#62867a\" stroke-width=\"1\"/>\n    <text id=\"footer\" x=\"48\" y=\"482\" fill=\"#d2efb0\" font-family=\"Arial\" font-size=\"20\">A LITTLE CARD. A WORLD OF POSSIBILITIES.</text>\n  </svg>\n  <svg id=\"rear-face\" data-surface=\"rear\" data-label=\"Rear\" data-width-mm=\"85.6\" data-height-mm=\"54\" viewBox=\"0 0 856 540\" x=\"872\" y=\"0\" width=\"856\" height=\"540\">\n    <rect id=\"rear-bg\" width=\"856\" height=\"540\" rx=\"24\" fill=\"#eeeadd\"/>\n    <text id=\"rear-heading\" x=\"48\" y=\"90\" fill=\"#143e39\" font-family=\"Arial\" font-size=\"30\" font-weight=\"700\">GOOD THINGS START WITH AN IDEA.</text>\n    <text id=\"rear-name\" x=\"48\" y=\"170\" fill=\"#143e39\" font-family=\"Arial\" font-size=\"28\" data-field=\"holderName\">Alex Morgan</text>\n    <text id=\"rear-note\" x=\"48\" y=\"460\" fill=\"#52655b\" font-family=\"Arial\" font-size=\"20\">Keep creating. This is your space.</text>\n  </svg>\n</svg>"
}